为什么这个 Thread 代码有时会打印出错误的意外结果?

Why this Thread code print wrong unpredicted result sometimes?

我是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个线程共享的计数器。线程只是重复加 1,减 1 到计数器值。所以,如果我猜的话,结果应该是0。因为初始值为0,递增和递减的数量是相同的。但有时最后打印的数字不是0,而是-1或-2等。请解释这是为什么。

这里有两个问题。它们是并发的原子性和可见性方面。自增和自减都是复合动作,需要在多线程环境下以原子方式执行。除此之外,无论何时读取计数器,您都不应该读取陈旧的值。 None 您当前的实施保证了这些。

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

更好的方法是使用基于 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,它以我们这个时代的一些伟大作家所著,以非常优美、易懂的方式解释了所有这些基础知识!如果您对本书的内容有任何疑问,也欢迎您在这里 post 提问。从头到尾至少读两遍!

更新

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

正确。

AtomicInteger

我更喜欢的替代解决方案是使用 Atomic… classes。具体在这里,AtomicInteger。 class 是一个线程安全的整数包装器。

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

然后您可以完全丢弃 Counter2 class。

执行者

此外,您应该知道,在现代 Java 中,我们很少需要直接解决 Thread class。相反,我们使用添加到 Java 5.

的执行器框架

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

扩展

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