我找到了关于线程安全的代码,但给出示例的人没有任何解释。我想了解为什么如果我不在“计数”之前设置“同步”变量,计数值将是非原子的(总是 =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);
}
}
答案 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 一书,每个开发人员都应该阅读这本书。