在理解volatile以前,咱们先来看下CPU的工做模式:
处理器这种工做产生的问题:
一、全部的变量在处理器运算期间都是变量对应值的一个副本,其它处理器没法感知其对变量的操做。
二、处理器为了高效利用寄存器而对指令的重排在多线程下将会产生没法预测的结果。
三、不一样的处理器针对同一套编码所产生的指令会有不一样的运行策略。
为了解决上述三个问题JVM为了保证每一个平台代码运行结果的一致性提出了JMM(JAVA内存模型),目的是为了让Java程序在各类平台下都能达到一致性的结果。
JMM规范:
Happen-Before原则:
一、程序顺序原则:一个线程内保证语意的串行化
二、volatile规则:volatile变量的写先发生于读,这保证了volatile变量的可见性
三、锁规则:解锁必然发生于加锁前
四、传递性:A先于B,B先于C,A必定先于C
五、线程的start()方法先于它的每个动做
六、线程的全部动做,先于线程的终结
七、线程的中断先于被中断的代码
八、对象的构造函数执行、结束先于finalize()方法
针对volatile的优化:
volatile能保证修改对其它线程可见。即修改了共享变量后确定会刷回主内存,通知其它线程,可是为了使处理器的内部单元高效工做,处理器会对输入的代码进行乱序即指令重排。对于volatile若是不作针对性的处理,那显然volatile的可见性并不会有什么意义。并不能保证结果的肯定性。
针对volatileJVM作了大量的工做:
关于工做内存(针对硬件就是高速缓存)JMM定义了8种操做来完成:
- lock(加锁): 做用于主内存,把一个变量标记为线程独占。
- unlock(解锁):做用于主内存,把一个已锁定的变量释放出来。
- read(读取):做用于主内存,将一个变量从主内从中传输到工做内存中,以便随后的load。
- load(载入):做用于工做内存,把read操做获得的变量放在工做内存的变量副本中。
- use(使用):做用于工做内存,把工做内存中的一个变量传递给执行引擎。
- assign(赋值):做用于工做内存,把一个执行引擎接受的值赋值给工做内存的变量。
- store(存储):做用于工做内存,把工做内存中的一个变量的值传输到主内存,以便后续的write操做。
- write(写入):做用于主内存,把store操做从工做内存获得的值放回主内存中。
8中操做有以下关系:
- 不容许load和read,store和write单独出现。
- 不容许一个线程丢弃它最近的assign操做,即变量在工做内存中改变,必须同步回主内存。
- 不与许一个线程无缘由的(没有assign操做)把数据从工做内存同步回主内存。
- 一个新的变量只能在主内存中诞生。
- 一个变量只能同时有一个线程进行加锁。lock能够被同一个线程加锁屡次,可是必须解锁相同次数。这个变量才会被解锁。
- 对一个变量执行lock操做。将会先清空该线程的工做内存中的该变量的值。在执行引擎使用这个变量前,须要从新执行load或assign操做。
- 一个变量被lock,不容许其它线程执行unlock。也不容许执行unlock被别的线程lock的变量。即一个线程本身lock的只有本身能unlock.
- 一个变量unlock以前,工做内存中的数据必须同步回主内存。
这八种操做和其使用规则,决定了变量在工做内存和主内存之间的同步策略。
针对于volatile变量又有额外以下定义:
- volatile变量在use时,必须执行load操做。即每次使用volatile变量必须先从主内存中刷新最新值。
- volatile变量在assign时,必须执行write操做。即每次对volatile进行赋值操做必须立马同步回主内存。
针对volatile和普通变量,或者volatile变量和volatile变量一块儿使用时。
JVM在编译期间也会针对volatile的重排加以干涉,干涉规则以下:

- 若是第二个操做时volatile写操做,无论第一操做是什么操做,都不能重排。
- 若是第一个操做时volatile读操做,无论第二个操做时什么操做,都不能重排。
- volatile写和volatile读不能重排。
为了实现这个语意,JVM在生成字节码时,会在指令序列中插入内存屏障(memory barrier)来禁止特定类型的处理器指令重排,对于编译器来讲对全部的CPU来插入屏障数最小的方案几乎不可能,下面是基于保守策略的JMM内存屏障插入策略:
- 在每一个volatile写操做前面插入StoreStore屏障
- 在每一个volatile写操做后插入StoreLoad屏障
- 在每一个volatile读后面插入一个LoadLoad屏障
- 在每一个volatile读后面插入一个LoadStore屏障
这里要说下内存屏障是是什么东西:硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障,内存屏障的做用有两个:
- 阻止屏障两侧的的指令重排
- 强制把高速缓存中的数据更新或者写入到主存中。Load Barrier负责更新高速缓存, Store Barrier负责将高速缓冲区的内容写回主存
LoadLoad,StoreStore,LoadStore,StoreLoad其实是Java对上面两种屏障的组合,来完成一系列的屏障和数据同步功能:
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操做要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操做执行前,保证Store1的写入操做对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操做被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续全部读取操做执行前,保证Store1的写入对全部处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
- StoreStore屏障能够保证在volatile写以前,全部的普通写操做已经对全部处理器可见,StoreStore屏障保障了在volatile写以前全部的普通写操做已经刷新到主存。
- StoreLoad屏障避免volatile写与下面有可能出现的volatile读/写操做重排。由于编译器没法准确判断一个volatile写后面是否须要插入一个StoreLoad屏障(写以后直接就return了,这时其实不必加StoreLoad屏障),为了能实现volatile的正确内存语意,JVM采起了保守的策略。在每一个volatile写以后或每一个volatile读以前加上一个StoreLoad屏障,而大多数场景是一个线程写volatile变量多个线程去读volatile变量,同一时刻读的线程数量其实远大于写的线程数量。选择在volatile写后面加入StoreLoad屏障将大大提高执行效率(上面已经说了StoreLoad屏障的开销是很大的)。
- LoadLoad屏障保证了volatile读不会与下面的普通读发生重排
- LoadStore屏障保证了volatile读不回与下面的普通写发生重排。
即便JMM对volatile作了这么多的工做,它也仅仅只保证了volatile变量在原子性操做下多个线程之间的正确同步,对非原子操做,使用volatile仍然会发生没法预知的结果。
好比对i++操做,在多线程状况下结果依然是不定:
例子:
咱们来使用 javap -c 来看下这个文件的编译指令:
increase方法的编译指令咱们能够看出 ++ 操做经历了4步:
一、getstatic #10 获取静态变量num压入栈顶 此时volatile保证值是对的。
二、iconst_1 int型常量1入栈
三、iadd 栈顶两个int值相加,结果放入栈顶。
四、putstatic #10 把栈顶的值负值给指定域。
问题就出在二、3两步,在作这两步操做时,volatile变量有可能已经被其它线程修改。
根据volatile的内存语意咱们能够总结出两条安全使用volatile的方式:
- 运算结果不依赖于volatile变量的当前值,或者能保证只有单一线程能修改变量的值
- 变量不须要与其它的状态变量共同参与不变性。