volatile内存语义
volatile的特性
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(基于这点,我们通过会认为volatile不具备原子性)。volatile仅仅保证对单个volatile变量 的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。 64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。
- 有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。 在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义: 严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存 语义。
volatile写-读的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,再从主内存中读取共享变量。
volatile可见性实现原理
-
JMM内存交互层面 volatile修饰的变量,其read,load,use,assign,store,write必须是连续的,也就是修改后必须立即同步回主内存,使用时必须立即从主内存刷新,由此保证volatile对变量操作在多线程环境中的可见性。 -
硬件层面 通过Lock前缀指令,锁定变量缓存行区域,并写回到主内存,也就是"缓存锁定",缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。 -
volatile在hotspot的实现 (字节码解释器实现) 可以看到,程序会先判断这个变量又没有没volatile修饰,有的话则调用OrderAccess:😗*storeload()**方法。 storeload,也就是JVM层面的内存屏障(要与处理器的内存屏障区分开哦~),storeload方法内部调用了fence方法,fence方法先判断处理器是否为多核,如果为多核则调用==lock; addl $0,0(%%rsp)==指令。 这个lock前缀指令并非内存屏障指令,由于它可以起到内存屏障的效果,还可以使缓存失效,并且其性能也要优于mfence内存屏障,所以选用它。那么lock前缀指令究竟有哪些作用呢?
- 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低 lock前缀指令的执行开销。
- LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。
- LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新 store buffer的操作会导致其他cache中的副本失效。
?
什么是指令重排序
Java语言规范规定JVM线程内部维持顺序化语义。也就是说只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重 排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
在编译器与CPU处理器中都能执行指令重排优化操作
volatile重排序规则
volatile禁止重排序场景
- 第二个操作是volatile写,不管第一个操作是什么都不会重排序
- 第一个操作是volatile读,不管第二个操作是什么都不会重排序
- 第一个操作是volatile写,第二个操作是volatile读,也不会重排序
JMM内存屏障插入策略
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
|