java多线程中的线程安全

时间:2021-07-10 06:46:20

标签: java multithreading

我找到了关于线程安全的代码,但给出示例的人没有任何解释。我想了解为什么如果我不在“计数”之前设置“同步”变量,计数值将是非原子的(总是 =200 是所需的结果)。谢谢

    public class Example {

     private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    for (int i = 0; i < 100; i++) {
                       //add synchronized
                        synchronized (Example.class){
                        count++;
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }
      }

3 个答案:

答案 0 :(得分:1)

++ 不是原子的

count++ 操作不是原子操作。这意味着它不是一个单独的操作。 ++ 实际上是三个操作:加载、增量、存储。

首先将存储在变量中的值加载(复制)到 CPU 内核的寄存器中。

其次,内核寄存器中的值递增。

第三个也是最后一个,新增加的值从内核的寄存器写(复制)回内存中变量的内容。然后内核的寄存器可以自由分配其他值用于其他工作。

完全有可能两个或多个线程读取变量的相同值,例如 42。然后,这些线程中的每一个将继续将该值增加到相同的新值 43。然后他们每个人都会将 43 写回同一个变量,不知不觉地一次又一次地重复存储 43

添加 synchronized 消除了这种竞争条件。当第一个线程获得锁时,第二个和第三个线程必须等待。所以第一个线程保证能够单独读取、递增和写入新值,从 42 到 43。一旦完成,该方法退出,从而释放锁。争夺锁的第二个线程获得批准,获取锁,并且能够不受干扰地读取、增加和写入新值 44。等等,线程安全。

另一个问题:可见性

然而,这段代码仍然被破坏了。

此代码存在可见性问题,各种线程可能会读取缓存中保存的陈旧值。但那是另一个话题。搜索以了解有关 volatile 关键字、AtomicInteger 类和 Java 内存模型的更多信息。

答案 1 :(得分:1)

<块引用>

我想了解为什么如果我不在“计数”之前设置“同步”变量,计数值将是非原子的。

简短的回答:因为 JLS 是这么说的!

如果您使用 synchronized(或 volatile 或类似的东西),那么 Java 语言规范 (JLS) 不保证主线程将看到子线程写入 count 的值。

这在 JLS 的 Java 内存模型部分中有详细说明。但规范是非常技术性的。

简化版本是,如果没有发生在之前(HB)关系连接写入和阅读。然后有一堆规则说明何时存在 HB 关系。规则之一是在释放互斥锁的线程和获取互斥锁的不同线程之间存在 HB。

另一种直观(但不完整且技术上不准确)的解释是,count 的最新值可能缓存在寄存器或芯片组的内存缓存中。 synchronized 构造将值刷新为内存。

解释不准确的原因是 JLS 没有说明任何关于寄​​存器、缓存等的内容。相反,内存可见性保证了 JLS 指定的通常实现由 Java 编译器插入指令以将寄存器写入内存、刷新缓存,或硬件平台所需的任何内容 .


另一件需要注意的事情是,这与 count++ 是否具有原子性无关1。这是关于更改 count结果是否对不同的线程可见。

1 - 它不是原子的,但是对于原子操作,您会得到与简单赋值相同的效果!

答案 2 :(得分:0)

让我们以华尔街的例子回到基础。

比方说,你(让我们打电话给 T1)和你的朋友(让我们打电话给 T2)决定在华尔街的一家咖啡馆见面。你们俩同时开始,比方说从华尔街的南端(虽然你们不是一起走的)。你在人行道的一侧醒来,你的朋友正在华尔街的人行道的另一边走,你们都朝北走(方向相同)。

现在,假设您来到一家咖啡馆前,您认为这是您和您的朋友决定见面的咖啡馆,因此您走进咖啡馆,点了一杯冷咖啡,并在等待时开始啜饮。

但是,在马路的另一边,发生了类似的事情,你的朋友来到一家咖啡店,点了一杯热巧克力,正在等你。

过了一会儿,你们俩都决定对方不会来了,放弃了见面的计划。

你们都错过了目的地和时间。为什么会这样?不用说但是,因为你没有决定确切的地点。

代码

synchronized(Example.class){
 counter++;
}

解决您和您朋友刚刚遇到的问题。

从技术角度来说,counter++的操作实际上是分三步进行的;

Step 1: Read the value of counter (lets say 1)
Step 2: Add 1 in to the value of counter variable.
Step 3: Write the value of the variable counter back to memory.

如果两个线程同时在计数器变量上工作,计数器的最终值将是不确定的。例如,线程 1 可以读取计数器的值为 1,同时线程 2 可以读取变量的值为 1。两个线程最终都会将计数器的值递增为 2。这称为竞争条件。

为了避免这个问题,操作 counter++ 必须是原子的。要使其具有原子性,您需要同步线程的执行。每个线程应以有组织的方式修改计数器。

我建议你阅读 Java Concurrency In Practice 一书,每个开发人员都应该阅读这本书。