【问题标题】:Java Multithreading - Threadsafe CounterJava 多线程 - 线程安全计数器
【发布时间】:2015-07-05 04:42:55
【问题描述】:

我从一个非常简单的多线程示例开始。我正在尝试制作一个线程安全的计数器。我想创建两个线程,间歇性地增加计数器达到 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

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

【问题讨论】:

  • 为什么你认为while循环意味着这个?
  • 没有什么可以“修复”的。在线程之间切换成本很高,并且调度程序将不会交替地让线程运行以进行对incrementCounter 的单个调用。将循环变量从 1000 增加到 1000000,然后您会看到线程确实确​​实在两者之间发生了变化。
  • @Marco13 虽然这是一个原始示例,确实没有必要在线程之间切换,但存在这样的短而关键的任务由于一个线程占用监视器而没有执行的情况,导致线程饥饿,可能会阻止进展。在这种情况下,通过实施公平政策,肯定有一些事情需要“解决”。
  • “修复”是什么意思?您的程序按预期工作。当多个线程并行运行时,JVM 没有义务为所有线程提供处理器时间。还是你期待别的?

标签: java multithreading thread-safety counter


【解决方案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 处于饥饿状态。

    解决方法是使用支持公平的并发同步方式(如ReentrantLock),如下图:

    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();          
        }
    }
    

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

    【讨论】:

      【解决方案3】:

      好吧,使用您的代码,我不知道如何间歇性地“准确”获取,但如果您在调用 incrementCounter() 后使用 Thread.yield(),您将获得更好的分布。

      public void run() {
               while(counter<1000){
                    incrementCounter();
                    Thread.yield();
      
               }
          }
      

      否则,要获得您的建议,您可以创建两个不同的线程类(如果需要,可以创建 ThreadsExample1 和 ThreadsExample2),并将另一个类作为共享变量。

      public class SharedVariable {
          private int value;
          private boolean turn; //false = ThreadsExample1 --> true = ThreadsExample2
      
          public SharedVariable (){
              this.value = 0;
              this.turn = false;
          }
      
          public void set (int v){
              this.value = v;
          }
      
          public int get (){
              return this.value;
          }
      
          public void inc (){
              this.value++;
          }
      
          public void shiftTurn(){
              if (this.turn){
                  this.turn=false;
              }else{
                  this.turn=true;
              }
          }
      
          public boolean getTurn(){
              return this.turn;
          }
      
      }
      

      现在,主要可以是:

      public static void main(String[] args) {
              SharedVariable vCom = new SharedVariable();
              ThreadsExample1 hThread1 = new ThreadsExample1 (vCom);
              ThreadsExample2 hThread2 = new ThreadsExample2 (vCom);
      
              hThread1.start();
              hThread2.start();
      
              try {
                  hThread1.join();
                  hThread2.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
          }
      

      你必须改变你的线路static int counter = 1; // a global counterprivate SharedVariable counter;

      新的运行是:

      public void run() {
          for (int i = 0; i < 20; i++) {
              while (!counter.getTurno()){
                  Thread.yield();
              }
              System.out.println(this.counter.get());
              this.counter.cambioTurno();
          }
      }
      

      }

      是的,它是另一个代码,但我认为它可以帮助你一点。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-09-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-11-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多