在整个并发知识体系(不断扩充)下,本文主要讨论的内容:
一、简介
volatile可以说是java虚拟机提供的最轻量级的同步机制,因为它不会引起线程上下文的调度和切换,执行成本比synchronized低。Java内存模型(JMM)告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。volatile适用于一个写多个读的情况。
二、volatile实现原理
- Lock前缀指令
- MESI协议
在此处要引入一个硬件知识,就是CPU Cache(高速缓存),计算机的存储层次为寄存器、cpu cache、内存、磁盘。cpu不会直接和内存进行交互,他们之间有一个cpu cache作为缓存,提高系统的运行速度。
一个volatile变量,在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令
volatile实现原理:
- 1、Lock指令会引起处理器将缓存写回到内存
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存 - 2、使用EMSI或嗅探机制保证缓存一致性
但是写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就需要缓存一致性协议:MESI、总线嗅探机制。不懂这两个名词的可以自行查阅,涉及到cpu cache缓存一致性知识,还可以拓展cache伪共享问题,cpu cache 存储结构,这里不再详细叙述。
2.1 可见性
上面的讲解其实也就是保证了volatile的可见性。
锁的happens-before锁规则,保证了释放锁和获取锁的两个线程间的内存可见性,也就是说对一个volatile变量的读,总能看到(任意线程)最后一次对这个变量的写。
happens-before规则有一个volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。这个规则保证了可见性和有序性。
那么从内存语义的角度来讲,volatile的写-读和锁的释放-获取具有相同的内存效果。
三、volatile写-读的内存语义
假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。
public class VolatileExample {
private int a = 0;
private volatile boolean flag = false;
public void writer(){
a = 1; //1
flag = true; //2
}
public void reader(){
if(flag){ //3
int i = a; //4
}
}
}
当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。
从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。(EMSI协议)
3.1 volatile内存语义的实现
volatile内存语义的实现是由读写屏障来实现的。
内存屏障中的读写屏障——并发问题
一文解决内存屏障
JMM内存屏障分为四类见下图,
java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
下面以两个示意图进行理解,图片摘自相当好的一本书《java并发编程的艺术》。
四、可见性vs原子性
- Synchronized可保证代码块的原子性,但是其性能相对较低
参考资料
- 《Java并发编程的艺术》
- 《深入理解Java虚拟机》
- 黑马JUC