本章是在学习内存模型后,对Volatile关键字 有了更加全面得理解,对知识点进行一个分析总结。
volatile在单个操作上和synchronized一样
本节采用happens-before关系阐述volatile的作用
int a;
volatile boolean flag;
public void init(){
a = 1; //①
flag = true; // ②
}
public void doTask(){
if(flag){ // ③
result = a; // ④
}
......
}
假设线程A执行init(), 线程B执行doTask();这个过程里,happens-before关系共分为三类:
这个过程的happens-before关系图如下所示:
分别遵守程序次序规则、volatile变量规则和传递规则:
JMM针对编译器制定的volatile的重排序规则
当volatile写发生时,本地内存将刷新主内存。就拿上面happens-before的例子来说,当A线程执行init()写入volatile变量后,B线程执行doTask()读取volatile变量。内存状态变化图如下所示
线程A写入flag变量后,本地内存将更新的共享变量(更新了几个就刷新几个)刷新至主内存,此时A线程的本地内存和主内存的共享变量相同。
当B线程要读取flag变量时,本地内存B 中包含的共享变量已经被置为无效,B线程不得不去主内存读取共享变量。线程B的读取将导致本地内存B与主内存的共享变量的值变成一致。
将两张图综合起来看,在读线程B读取一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量都将立即变得对读线程B可见。
volatile关键字实现原理主要还是通过内存屏障进行控制的。编译器在生成字节码时,会在指令序列里插入内存屏障来禁止特定的重排序。对于编译器来说,自行判断最小化插入屏障总数不太可能。为此,JMM采取保守策略:
上面的插入策略十分保守,但它可以保证在任意处理器平台上(在X86里,写/写,读/读,读/写 是不会发生重排序的,而且只有StoreLoad一个内存屏障),任意的程序中都能实现正确的语义。
下面是保守策略下,volatile写插入内存屏障的指令序列示意图。
StoreStore保证在执行volatile写前,所有写操作的处理已经刷新至内存,保证对其他线程可见了。而StoreLoad的作用是避免后面还有其他的volatile读/写操作发生重排序。由于JMM无法准确判断StoreLoad所处的环境(比如结尾是return),所以有两种选择:
但是因为StoreLoad相比其他内存屏障更加消耗性能,考虑更多场景下是少写多读,所以将StoreLoad加在volatile写后。
讲到StoreLoad的性能问题,不得不提一下Unsafe里面的putOrderedObject()。 这个方法很有意思,乍一看命名是放一个有序的对象,但它是通过避免加上StoreLoad内存屏障来弥补volatile写的性能问题。这时可能会有朋友问,不加上volatile不会影响可见性吗?会影响可见性,但不会永远影响下去,最多就两三秒的延迟,就会将共享变量刷新至主内存。所以当延迟要求不高,性能要求高时,就可以采用这个方法(Unsafe不安全类,这个方法的实现在Atmoic***类里面)。
下面是保守策略下,volatile读插入内存屏障的指令序列示意图。
LoadLoad保证先执行volatile读再执行后续的读操作(禁止volatile读和后续的读发生重排序),而后的LoadStore保证先执行volatile读再执行写操作(禁止volatile读和后续的写发生重排序)。两者联合起来就是无论如何volatile读必须和程序顺序保持一致。
上面的volatile读/写的内存屏障插入策略都十分保守,但是在实际过程中,只要不改变volatile写/读的内存语义,编译器可以根据实际情况省略不必要的屏障。
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite(){
int i = v1;
int j = v2;
a = i + j;
v1 = i + 1;
v2 = j + 2;
}
针对readAndWrite()方法,编译器在生成字节码时会做如下优化。
按顺序下来,第一个volatile读先于第二个volatile,第二个volatile先于所有后续的写,故第一个volatile读一定不会被重排序;StoreStore保证普通写先于第一个volatile写,StoreStore又保证第一个volatile写先于第二个volatile写,最后安全起见插入StoreLoad。
上面的优化针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。比如X86处理器,由于X86不会对读/读,读/写,写/写做重排序,所以面对X86处理器时,JMM会省略掉三种类型对应的内存屏障,保留StoreLoad内存屏障。
在之前的版本,虽然不允许volatile变量间 的重排序,但是允许volatile和普通变量间的重排序。为了提供一种比锁更轻量级的线程间通信机制,专家组决定增强volatile的内存语义,严格限制volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。
由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和性能上,volatile更有优势。具体看《Java理论与实践:正确使用volatile变量》
原文:https://www.cnblogs.com/codeleven/p/10963117.html