volatile内存屏障
volatile
volatile是Java虚拟机提供的轻量级的同步机制。
- 不能保证原子性
- 保证volatile修饰的共享变量对所有线程是可见的。也就是当一个线程修改 了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
- 禁止指令重排序
禁止指令重排的原因
volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱 序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如 何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。
硬件层的内存屏障
Intel硬件提供了一系列的内存屏障,主要有:
- lfence,是一种Load Barrier 读屏障
- sfence, 是一种Store Barrier 写屏障
- mfence, 是一种全能型的屏障,具备ifence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对 CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由 JVM来为不同的平台生成相应的机器码。 JVM中提供了四类内存屏障指令:
- 内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执 行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于 编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉 编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插 入内存屏障禁止在内存屏障前后的指令执行重排序优化。
- Memory Barrier的另外一个作用 是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。 总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。
volatile内存语义的实现
前面提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义, JMM会分别限制这两种类型的重排序类型。
下图是JMM针对编译器制定的volatile重排序规则表。
举例来说,第二行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或写 时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
- 当第二个操作是volatile写时,不管第一个操作是什么,都不 能重排序。这个规则确保volatile写之前的操作不会被编译器重排序 到volatile写之后
- 当第一个操作是volatile读时,不管第二个操作是什么,都不 能重排序。这个规则确保volatile读之后的操作不会被编译器重排序
到volatile读之前。 - 当第一个操作是volatile写,第二个操作是volatile读时,不能 重排序。
实现volatile的内存语义
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障 来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的 总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略
- ∙在每个volatile写操作的前面插入一个StoreStore屏障。
- ∙在每个volatile写操作的后面插入一个StoreLoad屏障。 ∙
- 在每个volatile读操作的后面插入一个LoadLoad屏障。 ∙
- 在每个volatile读操作的后面插入一个LoadStore屏障
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到 正确的volatile内存语义。
volatile写插入内存屏障后生成的指令序列示意图
上图中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任 意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新 到主内存。
这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile 写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile 写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即 return)。为了保证能正确 实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效 率的角度考虑,JMM最终选择了在每个 volatile写的后面插入一个StoreLoad屏障。因为 volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量,多个读线程读同一 个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad 屏障将带来可观的执行效率的提升。从这里可以看到JMM 在实现上的一个特点:首先确保 正确性,然后再去追求执行效率。
volatile读插入内存屏障后生成的指令序列示意图
上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。 LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示 例代码进行说明
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // 第一个volatile读
int j = v2; // 第二个volatile读
a = i + j; // 普通写
v1 = i + 1; // 第一个volatile写
v2 = j * 2; // 第二个 volatile写 11 } 12 }
针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化。