中心思想
使普通全局变量的写对其他线程立即可见(使用volatile有序性来传递)
内存屏障
先来一堆有必要的废话
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。
为了实现volatile的有序性内存语义(jdk5之后),编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的前面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障
volatile写操作插入内存屏障后生成的指令序列如下图所示。
volatile读操作插入内存屏障后生成的指令序列如下图所示。
传递性 :如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
相信大家对上面的内容很熟悉了,但是代表啥意思呢,估计你还是一头雾水。我们举个例子
int e = 0;
volatile f = 0;
//thread 1
e = 1; //操作 A
f = 1; //操作 B
//thread 2
int j = f; //操作 C
int k = e; //操作 D
我们假设 thread 1 执行结束之后thread 2执行,由于f是volatile变量,那么可知B操作的写,对于C操作的读是可见的。那么可得B先发生与C,每个线程单独来看, A先发生与B, C先发生与D,最后我们再加上传递性可知。
A->B->C->D
至此我们可以得到普通变量e的的写入对于其他线程立即可见(注意变量f在其中起到的作用)
Reentrantlock 也借助了volatile的这个特性
总结一下有序性的含义
- 禁止指令重排
- volatile写会将线程工作缓存(cpu缓存)中的所有数据写入主存
- volatile读会将线程工作缓存(cpu缓存)中的所有数据失效,读的时候需要从主存中取。
测试代码
前面举的例子比较难测试出反例(对普通变量的写,不通过volatile的有序性保证,其他线程不是立即可见),所以写了下面的测试代码。
static int a = 0;
static volatile int b = 0; //去掉volatile修饰会发生死循环,即变量a对于其他线程不是立即可见
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while(a==0) {
int c = b;
}
});
thread.start();
Thread.sleep(200);
new Thread(() -> {
a = 1;
b = 1;
}).start();
thread.join();
}