Java 多线程 - 线程安全计数器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/29883719/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 08:45:44  来源:igfitidea点击:

Java Multithreading - Threadsafe Counter

javamultithreadingthread-safetycounter

提问by BrotherBarnabas

I'm starting off with a very simple example in multithreading. I'm trying to make a threadsafe counter. I want to create two threads that increment the counter intermittently to reach 1000. Code below:

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

From what I can tell, the while loop right now means that only the first thread has access to the counter until it reaches 1000. Output:

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

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

How do I fix that? How can I get the threads to share the counter?

我该如何解决?如何让线程共享计数器?

采纳答案by initramfs

Both threads have access to your variable.

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

The phenomenon you are seeing is called thread starvation. Upon entering the guarded portion of your code (sorry I missed this earlier), other threads will need to block until the thread holding the monitor is done (i.e. when the monitor is released). Whilst one may expect the current thread pass the monitor to the next thread waiting in line, for synchronized blocks, java does not guarantee any fairness or ordering policy to which thread next recieves the monitor. It is entirely possible (and even likely) for a thread that releases and attempts to reacquire the monitor to get hold of it over another thread that has been waiting for a while.

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

From Oracle:

来自甲骨文:

Starvation describes a situation where a thread is unable to gain regular access to shared resources and is unable to make progress. This happens when shared resources are made unavailable for long periods by "greedy" threads. For example, suppose an object provides a synchronized method that often takes a long time to return. If one thread invokes this method frequently, other threads that also need frequent synchronized access to the same object will often be blocked.

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

Whilst both of your threads are examples of "greedy" threads (since they repeatedly release and reacquire the monitor), thread-0 is technically started first, thus starving thread-1.

虽然您的两个线程都是“贪婪”线程的示例(因为它们反复释放并重新获取监视器),但从技术上讲,线程 0 首先启动,因此线程 1 处于饥饿状态。

The solution is to use a concurrent synchronization method that supports fairness (e.g. ReentrantLock) as shown below:

解决方案是使用支持公平性的并发同步方法(例如 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();          
    }
}

note the removal of the synchronizedkeyword in favor of the ReentrantLock within the method. Such a system, with a fairness policy, allows long waiting threads a chance to execute, removing the starvation.

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

回答by almeynman

try to sleep a thread to ensure that the other will have a go:

尝试休眠一个线程以确保另一个线程可以运行:

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

回答by Bert Peters

You could use the AtomicInteger. It is a class that can be incremented atomically, so two seperate threads calling its increment method do not interleave.

你可以使用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();          
     }
}

回答by Shondeslitch

Well, with your code I don't know how to get "exactly" intermittently, but if you use Thread.yield()after call incrementCounter()you will have a better distribution.

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

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

         }
    }

Otherwise, to get what you propose, you can create two different thread class (ThreadsExample1 and ThreadsExample2 if you want), and another class to be a shared variable.

否则,为了获得您的建议,您可以创建两个不同的线程类(如果需要,可以创建 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;
    }

}

Now, the main can be:

现在,主要可以是:

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

    }

And you have to change your line static int counter = 1; // a global counterfor private SharedVariable counter;

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

And the new run is:

新的运行是:

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

}

}

Yes, it is another code, but I think it can help you a little bit.

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