volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
对一个 volatile 变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同;也就是说对一个 volatile 变量的读,总是能看到(任意线程) 对这个 volatile 变量最后的写入。
值得注意的是:Java 内存模型对 volatile 语义的扩展保证了 volatile 变量在一些情况下不会重排序,volatile 的 64 位变量 double 和 long 的读取和赋值操作都是原子的。如果是多个 volatile 操作或类似于 volatile++ 这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile 变量自身具有一下特性:
当写一个 volatile 变量时,JMM 会把该线程栈中的共享变量值刷新到主内存,使主内存和该线程栈中的共享变量的值是一致的。
当读一个 volatile 变量时,JMM 会把该线程栈中的共享变量的值置为无效,随后线程会从主内存中重新读取共享变量。
下面对 volatile 写和 volatile 读的内存语义做个总结:
简而言之:线程 A 修改了 volatile 变量,通过主内存发送消息到下一个要读 volatile 变量的线程;然后读的这个线程会将线程栈中的这个 volatile 变量置为无效,然后重新从主内存中读取。
volatile 实现 JVM 必须遵循以下重排序规则:
从上表我们可以看出:
留白的单元格代表允许在不违反Java基本语义的情况下重排序。例如,编译器不会对对同一内存地址的读和写操作重排序,但是允许对不同地址的读和写操作重排序。
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止听顶类型的处理器重排序。下面是基于保守策略的 JMM 内存屏障插入策略:
下面是保守策略下,volatile 写插入内存屏障后生成的指令序列示意图:
上图中的 StoreStore 屏障可以保证在 volatile 写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为 StoreStore 屏障将保障上面所有的普通写在 volatile 写之前刷新到主内存。
StoreLoad 屏障避免 volatile 写与后面可能有的 volatile 读/写操作重排序。因为编译器常常无法准确判断在一个 volatile 写的后面,是否需要插入一个 StoreLoad 屏障(比如,一个 volatile 写之后方法立即 return)。为了保证能正确实现 volatile 的内存语义,JMM 在这里采取了保守策略:在每个 volatile 写的后面或在每个 volatile 读的前面插入一个 StoreLoad 屏障。从整体执行效率的角度考虑,JMM 选择了在每个 volatile 写的后面插入一个 StoreLoad 屏障。
下面是在保守策略下,volatile 读插入内存屏障后发生的指令序列示意图:
上图的 LoadLoad 屏障用来禁止处理器把上面的 volatile 读与下面的普通读重排序。LoadStore 屏障用来禁止处理器吧上面的 volatile 读与下面的普通写重排序。