volatile原理
内存屏障(写前读后)
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence), 对 volatile 变量的写指令后会加入写屏障 对 volatile 变量的读指令前会加入读屏障
可见性: 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
有序性: 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
再来看三大特性的原理
保证可见性
volatile怎么保证的可见性? 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
在这点上,volatile与锁具有相同的内存效果,:
- volatile变量的写和锁的释放具有相同的内存语义
- volatile变量的读和锁的获取具有相同的内存语义。
禁止重排序
指令重排本意是为了提高程序执行的效率(调整指令执行的顺序到达执行一条指令的同时执行其他指令的目的)。单线程下不存在安全问题,但是多线程下存在。
指令重排序也遵循一定的规则: 重排序不会对存在依赖关系的操作进行重排 重排序只会对不存在依赖关系的操作进行重排 如下代码所示:
指令重排序现象(以后再看)
I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种? 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了) 但我告诉你,结果还有可能是 0 这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2相信很多人已经晕了 这种现象叫做指令重排
禁止指令重排原理
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
下面的有难度。 在旧的内存模型中,volatile的写-读就不能与锁的释放-获取具有相同的内存语义了。为了提供一种比锁更轻量级的线程间的通信机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序。
编译器还好说,JVM是怎么还能限制处理器的重排序的呢?它是通过内存屏障来实现的。
硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。 内存屏障有两个作用: 1、阻止屏障两侧的指令重排序; 2、强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。 注意这里的缓存主要指的是CPU缓存,如L1,L2等
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:
- 在每个volatile写操作前插入一个StoreStore屏障;
- 在每个volatile写操作后插入一个StoreLoad屏障;
- 在每个volatile读操作后插入一个LoadLoad屏障;
- 在每个volatile读操作后再插入一个LoadStore屏障
大概示意图是这个样子:
再逐个解释一下这几个屏障。注:下述Load代表读操作,Store代表写操作
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,这个屏障会吧Store1强制刷新到内存,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
对于连续多个volatile变量读或者连续多个volatile变量写,编译器做了一定的优化来提高性能,比如:
第一个volatile读;
LoadLoad屏障;
第二个volatile读;
LoadStore屏障
再介绍一下volatile与普通变量的重排序规则:
如果第一个操作是volatile读,那无论第二个操作是什么,都不能重排序;
如果第二个操作是volatile写,那无论第一个操作是什么,都不能重排序;
如果第一个操作是volatile写,第二个操作是volatile读,那不能重排序。 举例说明:
public class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
System.out.println(a);
}
}
}
step 1,是普通变量的写,step 2是volatile变量的写,那符合第2个规则,这两个steps不能重排序。而step 3是volatile变量读,step 4是普通变量读,符合第1个规则,同样不能重排序。
但如果是下列情况:第一个操作是普通变量读,第二个操作是volatile变量读,那是可以重排序的:
int a = 0;
volatile boolean flag = false;
int i = a;
boolean j = flag;
不保证原子性
为什么不保证原子性? 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到它前面去。 如下所示:volatile虽然可以保证t1线程的操作不被重排序,但是无法禁止t2在t1之前读i的值,所以最后还是会发生错误,不能保证原子性。
|