首先要知道,想要线程安全必须保证 原子性,可见性,有序性。
首先要了解的是, Volatile
禁止指令重排序(有序性),保证内存可见性问题,对 变量单个操作保证原子性, 那么它为什么不能保证线程安全呢?
下面首先会单独介绍什么是对变量单个操作保证原子性
JMM规定了内存主要划分为主内存和工作内存两种。每个线程都有一个自己的工作内存, 对变量的操作都是在缓存中进行的, 然后再将修改后的值返回到主存中,Java内存模型规定了所有的变量都存储在主内存(Main Memory)中 ,每条线程 还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图:
一个变量如何从主内存拷贝到工作内存、如何从 工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作来完成:
举个小小的例子:
如果我们在代码中对一个对象重新赋值,int race = 0
,那么随后就要将新的值同步到当前线程的工作内存,然后再从工作内存同步到主内存,这中间会经历Java内存模型中定义的三种操作use→assign→write
,然后可能会触发缓存一致性协议,使其他线程工作内存中的 race 变量全部失效,其他线程下次获取 race 变量值,都要从主内存中重新获取最新值。
但是,对于未使用volatile 修饰的变量而言,use→assign→write
,这三个操作并不是原子性的,这也就意味着其并不是线程安全的。假设有thread-1和thread-2两个线程,thread-1和thread-2工作内存中保留着 race 副本,thread-1对 race 进行了重新赋值,但是其最新值还未同步到主内存,此时thread-2线程读取 race 变量,那么就会直接命中其工作内存获取一个旧数据。
而 volatile 变量将read load use 三个原子操作变成一个原子操作;将assign-store-write变成一个原子操作,这也就意味着每次进行赋值操作都会将最新值刷新到工作内存,使其他线程对此变量的工作副本失效,下次读取必须从工作内存中获取最新值。这就是为什么讲对变量单个操作保证原子性。
volatile
变量在各个线程的工作内存中是不存在一致性问题的(从物理存储的角度看,各个线程的工作内存中volatile
变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看 不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算操作符并非原子操作,这导致volatile
变量的运算在并发下一样是不安全的.
下面来看一个例子
/**
* volatitle 变量自增运算测试
*/
public class VolatitleTest {
public static volatile int race = 0;
public static void increase(){
race++;
}
public static void main(String[] args) {
Thread[] threads = new Thread[20];
for (int i =0; i< threads.length;i++){
threads[i] = new Thread(()->{
for (int j = 0; j< 10000; j++){
increase();
System.out.println(race);
}
});
threads[i].start();
}
//等待所有累加线程都结束
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(race);
}
}
这段代码发起了20个线程, 每个线程对 race 变量进行 10000 次自增操作, 如果这段代码能够正确别难过发的话, 最后输出的结果应该是 200000; 但是最终结果都比 200000 要小??? 为什么呢???
问题就在 race++ , 用 Javap 反编译这段代码后会的到代码清单, 发现只有一行代码的 increase() 方法在 Class 文件中由4条字节码指令构成, 从字节码层面上已经很容易分析出并发失败的原因: 当 getstatic
指令吧 race 的值取到操作栈顶时候, volatitle
关键字保证了 race 的值此时是正确的. 但是在执行 inconst_1, iadd 这些指令的时候, 其他线程可能已经把 race 的值改变了, 而操作栈顶的值就变成了过期的数据, 所以 putstatic
指令 执行后, 就可能把较小的race 值同步回主内存之中.
public static void increase();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field race:I, 读取race 到栈顶
3: iconst_1
4: iadd
5: putstatic #2 // Field race:I
8: return
《Java并发编程的艺术》
《深入理解Java虚拟机》