【问题标题】:Why this Thread code print wrong unpredicted result sometimes? [duplicate]为什么这个线程代码有时会打印错误的意外结果? [复制]
【发布时间】:2021-08-22 04:19:41
【问题描述】:

我是java初学者,第一次使用线程。

class Counter2 {
    private int value = 0;

    public void increment() {
        value++;
        printCounter();
    }

    public void decrement() {
        value--;
        printCounter();
    }

    public void printCounter() {
        System.out.println(value);
    }

}

class MyThread3 extends Thread {
    
    Counter2 sharedCounter;

    public MyThread3(Counter2 c) {
        
        this.sharedCounter = c;

    }

    public void run() {
        int i = 0;

        while (i <= 100) {
            sharedCounter.increment();
            sharedCounter.decrement();
            
            try {
                sleep((int) (Math.random() * 2));
            } catch (InterruptedException e) {
            }
//          System.out.println(i);
            i++;
        }
    }
}

public class MyTest {

    public static void main(String[] args) {
        Thread t1, t2;
        Counter2 c = new Counter2();
        t1 = new MyThread3(c);
        t1.start();
        t2 = new MyThread3(c);
        t2.start();
        
    }

}


这段代码有2个线程和1个Counter,线程之间共享。线程只是重复加 1,减 1 到计数器值。所以,如果我猜,结果应该是 0。因为初始值为 0,并且递增和递减的数量是相同的。但有时最后打印的数字不是0,而是-1或-2等。请解释这是为什么。

【问题讨论】:

  • 您的价值不会波动,因此无法保证可见性。进一步的递增和递减运算符是复合赋值运算符的简写,它们不是原子的。如果没有外部同步,您的计数器就不是线程安全的。
  • 我已经将这个问题复制到了一个早期的带有计数器和线程的问题,这个问题本质上是相同的问题,但更简单一些。 (细节不同,但问题原因相同,解决方法相同。)
  • 我的建议:阅读有关 Java 线程安全的优秀教程。或者更好的是权威的布洛赫教科书(见下文)。这是一个通过反复试验无法弄清楚的话题。

标签: java multithreading counter


【解决方案1】:

这里有两个问题。它们是并发的原子性和可见性方面。递增和递减都是复合动作,需要在多线程环境中原子执行。除此之外,您不应在读​​取计数器时读取过时的值。您当前的实现无法保证这些。

回到解决方案,实现这一点的一种天真的方法是使用同步方法,该方法使用当前实例上的锁来实现线程安全。但这会带来相当高的成本,并且会产生更多的锁开销。

更好的方法是使用基于 CAS 的非阻塞同步来完成手头的任务。这是它在实践中的样子。

class Counter2 {
        private LongAdder value = new LongAdder();

        public void increment() {
            value.increment();;
            printCounter();
        }

        public void decrement() {
            value.decrement();;
            printCounter();
        }

        public void printCounter() {
            System.out.println(value.intValue());
        }

    }

由于您是初学者,我建议您阅读伟大的书Java Concurrency in Practice 1st Edition,它以非常好的方式解释了所有这些基础知识,我们这个时代的一些伟大作家可以理解!如果您对本书的内容有任何疑问,也欢迎您在这里提出问题。从头到尾至少读两遍!

更新

CAS 即所谓的 ComparaAndSwap 是一种使用低级 CPU 指令实现的无锁同步方案。在这里,它在递增/递减之前读取计数器的值,然后在更新时检查初始值是否仍然存在。如果是这样,它会成功更新该值。否则,可能是另一个线程同时更新计数器的值,因此递增/递减操作失败并再次重试。

【讨论】:

    【解决方案2】:

    Answer by Ranwala 是正确的。

    AtomicInteger

    我更喜欢的另一种解决方案是使用Atomic… 类。具体来说,AtomicInteger。这个类是一个整数的线程安全包装器。

    将您的成员字段从 Counter2 sharedCounter; 更改为 AtomicInteger sharedCounter;。然后使用该类上的各种方法来递增、递减和查询当前值。

    然后您可以完全放弃您的 Counter2 课程。

    执行者

    另外,您应该知道,在现代 Java 中,我们很少需要直接寻址 Thread 类。相反,我们使用 Java 5 中添加的执行器框架。

    将您的任务定义为RunnableCallable。无需从Thread扩展。

    查看 Oracle 的教程,并在 Stack Overflow 上搜索现有帖子。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-29
      • 2012-07-17
      • 2021-07-10
      • 2018-05-13
      • 2017-10-14
      • 2012-03-23
      相关资源
      最近更新 更多