关于线程安全保证主要靠三个特性保证:
1.原子性
2.可见性
3.禁止重排序
volatile关键字汇编指令为lock前缀指令,锁总线(大部分是锁缓存行),该指令意义:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效
简单讲就是通过总线缓存协议将其他的处理器中的相关缓存行置为无效以达到缓存一致性(MESI)。另外lock前缀指令有内存屏障作用及保证写操作(单个操作)原子性。
关于内存屏障
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:
- 在每个volatile写操作前插入一个StoreStore屏障;
- 在每个volatile写操作后插入一个StoreLoad屏障;
- 在每个volatile读操作后插入一个LoadLoad屏障;
- 在每个volatile读操作后再插入一个LoadStore屏障;
解释:
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
简单说,volatile与普通变量的重排序规则:
如果第一个操作是volatile读,那无论第二个操作是什么,都不能重排序;
如果第二个操作是volatile写,那无论第一个操作是什么,都不能重排序;
如果第一个操作是volatile写,第二个操作是volatile读,那不能重排序。
另外,happens-before原则中也有对volatile变量的写操作禁止重排序保证
happens-before八大原则:
- 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
- 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
- volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
- happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
- 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
- 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
- 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
- 对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。
关于重排序导致的线程不安全问题,比如单例创建对象中的双重检查锁:
初始化对象的指令可能会被重排成:
(1)分配内存空间。
(2)将内存空间的地址赋值给对应的引用。
(3)初始化对象
由于在java中, 如果共享变量的引用已经和构造器的调用内联了, 即使构造器未完成初始化, 共享变量也可能会立即被更新。所以其它线程可能会看到一个未经完全初始化的对象,所以要用到volatile的指令重排序功能解决该问题。(synchronized并不能完全解决重排序问题)
综上,volatile能保证可见性及禁止重排序,但是不能保证原子性(只能保证单个操作原子性)
举个例子:
i++操作可分为三步:
- 线程读取i
- temp = i + 1
- i = temp
当步骤2完成后其它线程刷新了主存时不再从主存读取i重新回走步骤1的情况,所以此时继续走步骤3时会有线程安全问题。
参考文档: