Java Multithreading - Threadsafe Counter

我从多线程中一个非常简单的例子开始。我正在尝试制作一个线程安全计数器。我想创建两个线程,间歇性地递增计数器以达到1000。代码如下:

public class ThreadsExample implements Runnable {
     static int counter = 1; // a global counter

     public ThreadsExample() {
     }

     static synchronized void incrementCounter() {
          System.out.println(Thread.currentThread().getName() + ": " + counter);
          counter++;
     }

     @Override
     public void run() {
          while(counter<1000){
               incrementCounter();
          }
     }

     public static void main(String[] args) {
          ThreadsExample te = new ThreadsExample();
          Thread thread1 = new Thread(te);
          Thread thread2 = new Thread(te);

          thread1.start();
          thread2.start();          
     }
}

据我所知,while 循环现在意味着只有第一个线程可以访问计数器,直到它达到 1000。输出:

Thread-0: 1
.
.
.
Thread-0: 999
Thread-1: 1000

我该如何解决这个问题?如何获取线程以共享计数器?


答案 1

您可以使用 .它是一个可以原子递增的类,因此调用其递增方法的两个单独线程不会交错。AtomicInteger

public class ThreadsExample implements Runnable {
     static AtomicInteger counter = new AtomicInteger(1); // a global counter

     public ThreadsExample() {
     }

     static void incrementCounter() {
          System.out.println(Thread.currentThread().getName() + ": " + counter.getAndIncrement());
     }

     @Override
     public void run() {
          while(counter.get() < 1000){
               incrementCounter();
          }
     }

     public static void main(String[] args) {
          ThreadsExample te = new ThreadsExample();
          Thread thread1 = new Thread(te);
          Thread thread2 = new Thread(te);

          thread1.start();
          thread2.start();          
     }
}

答案 2

两个线程都可以访问您的变量。

您看到的现象称为线程匮乏。进入代码的受保护部分后(抱歉我之前错过了这一点),其他线程将需要阻塞,直到保存监视器的线程完成(即当监视器被释放时)。虽然人们可能期望当前线程将监视器传递给下一个排队等待的线程,但对于同步块,java不保证任何公平性或排序策略,线程下一步接收监视器。对于释放并尝试重新获取监视器的线程来说,完全有可能(甚至可能)通过另一个等待了一段时间的线程来获取它。

来自甲骨文:

饥饿描述了线程无法定期访问共享资源并且无法取得进展的情况。当共享资源被“贪婪”的线程长时间不可用时,就会发生这种情况。例如,假设一个对象提供了一个同步方法,该方法通常需要很长时间才能返回。如果一个线程频繁调用此方法,则通常需要频繁同步访问同一对象的其他线程通常将被阻止。

虽然你的两个线程都是“贪婪”线程的例子(因为它们反复释放并重新获取监视器),但线程-0在技术上首先启动,从而使线程1挨饿。

解决方案是使用支持公平性的并发同步方法(例如,重入锁),如下所示:

public class ThreadsExample implements Runnable {
    static int counter = 1; // a global counter

    static ReentrantLock counterLock = new ReentrantLock(true); // enable fairness policy

    static void incrementCounter(){
        counterLock.lock();

        // Always good practice to enclose locks in a try-finally block
        try{
            System.out.println(Thread.currentThread().getName() + ": " + counter);
            counter++;
        }finally{
             counterLock.unlock();
        }
     }

    @Override
    public void run() {
        while(counter<1000){
            incrementCounter();
        }
    }

    public static void main(String[] args) {
        ThreadsExample te = new ThreadsExample();
        Thread thread1 = new Thread(te);
        Thread thread2 = new Thread(te);

        thread1.start();
        thread2.start();          
    }
}

请注意,删除了关键字,以支持方法中的 ReentrantLock。这样的系统具有公平政策,允许长时间等待的线程有机会执行,从而消除了饥饿。synchronized