java关键字volatile内存语义详细分析

volatile变量自身具有下列特性。
1.可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写 入。 ·
2.原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不 具有原子性。
(PS:从JSR-133内存模型开始(即从JDK5开始),仅仅只允许把 一个64位long/double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。总结:64为的变量写操作可能会被拆分两个32位的写操作,不具备原子性,读操作是原子性)
volatile 单个变量的读写可看做使用了同一个锁对单个读写做了同步。
 
volatile写-读的内存语义(JMM为java内存模型):
有volatile变量修饰的共享变量进行写操作的时候被处理成汇编指令序列会多出Lock前缀的指令,Lock前缀的指令在多核处理器下会引发了两件事情。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内 ,,实质上是线程A向接下来将要读这个volatile变量的某个线程 发出了(其对共享变量所做修改的)消息。
实现原则:Lock前缀指令会引起处理器缓存回写到内存:使用“缓存锁 定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
2.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主 内存中读取共享变量。线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。
实现原则:一个处理器的缓存回写到内存会导致其他处理器的缓存无效:处理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致 性。
(ps:线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过 主内存向线程B发送消息。)
 

volatile内存语义的实现

JMM会限制编译器重排序和处理器对volatile语义重排序。

volatile重排序规则表:

1.当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写        之前的操作不会被编译器重排序到volatile写之后。

2.当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。 

3.当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

 

为了实现volatile内存语义,编译器会在指令序列插入内存屏障来限制指令重排序。下面是基于保守策略的JMM内存屏障插入策略(关于内存屏障指令见文章末尾介绍):
1.在每个volatile写操作的前面插入一个StoreStore屏障。
2.在每个volatile写操作的后面插入一个StoreLoad屏障。
3.在每个volatile读操作的后面插入一个LoadLoad屏障。
4.在每个volatile读操作的后面插入一个LoadStore屏障。

 


volatile写插入内存屏障后生成的指令序列示意图:

 

 

这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确 实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个 volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时, 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。

volatile读插入内存屏障后生成的指令序列示意图:


上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例 代码进行说明。

 

 

针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化。


内存屏障类型表:

 

posted @ 2019-12-15 21:55  晋级在路上  阅读(368)  评论(0编辑  收藏  举报