java多线程中的线程安全



我找到了关于线程安全的代码,但给出示例的人没有对此进行任何解释。我想知道为什么如果我不设置;同步的";变量在";计数";计数值将是非原子的(总是=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);
}
}

++不是原子

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

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

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

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

两个或多个线程完全有可能读取变量的相同值,比如42。然后,这些线程中的每一个将继续将该值递增到相同的新值43。然后,它们各自将43写回同一变量,无意中一次又一次地重复存储43

添加synchronized消除了这种竞争条件。当第一个线程获得锁时,第二个和第三个线程必须等待。因此,保证第一个线程能够单独读取、增量和写入新值,从42到43。完成后,该方法退出,从而释放锁定。争夺锁的第二个线程获得了许可,获得了锁,并且能够在没有干扰的情况下读取、递增和写入新值44。等等,线程安全。

另一个问题:可见性

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

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

我想知道为什么如果我不设置;同步的";变量在";计数";计数值将是非原子的。

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

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

JLS的Java内存模型部分对此进行了详细说明。但是这个规格是非常技术性的。

简化版本是,如果在连接写入和读取的(HB)关系之前没有发生,则变量的读取不能保证看到前一次写入所写的值。然后有一堆规则来说明HB关系何时存在。其中一条规则是,在线程上释放互斥对象和另一个线程获取互斥对象之间存在HB。

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

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


另一件需要注意的事情是,这实际上与count++是否是原子无关。这是关于count更改的结果是否对其他线程可见。

1-它不是原子操作,但对于原子操作,就像简单的赋值一样,你会得到同样的效果

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

比方说,你(叫T1)和你的朋友(叫T2)决定在华尔街的一家咖啡馆见面。你们两个是在同一时间开始的,比方说从华尔街的南端开始(尽管你们并没有一起走)。你们在人行道的一侧醒来,你们的朋友在华尔街人行道的另一侧行走,你们都朝着北方走去(方向相同)。

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

但是,在路的另一边,也发生了类似的事件,你的朋友路过一家咖啡店,点了一份热巧克力,正在等你。

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

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

代码

synchronized(Example.class){
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.

若两个线程同时处理计数器变量,则计数器的最终值将是不确定的。例如,Thread1可以将计数器的值读取为1,同时thread2可以将变量的值读取1。两个线程都将计数器的值递增到2。这被称为竞赛条件。

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

我建议您阅读《Java并发实践》一书,每个开发人员都应该阅读这本书。

最新更新