当声明共享变量为volatile后,对这个变量的读/写就会变得很特别。
1. volatile的内存语义
- 保证变量的内存可见性
- 禁止volatile变量与普通变量的重排序
1.1. 内存可见性
所谓内存可见性,指的是当一个线程对volatile修饰的变量进行了写操作(比如step2)时,JMM会立即把改线程对应的本地内存中的共享变量刷新值到主内存;当一个线程对volatile修饰的变量进行读操作(比如step3)时,JMM会立即把该线程对应的本地内存置为无效,从主内存中读取共享变量的值。
public class VolatileDemo {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // step 1
flag = true; // step 2
}
public void reader() {
if (flag) { // step 3
System.out.println(a); // step 4
}
}
}
假如在时间线上,线程A先执行writer方法,线程B再执行reader方法。那必然会是下图:
如果flag变量没有用volatile修饰,在step2,线程A的本地内存里的变量就要不会立即刷新到主内存,而随后线程B也同样不会去主内存中拿最新的值,仍然使用线程B本地内存缓存的变量值a = 0,flag = false。
2. 禁止重排序
在旧的内存模型中,volatile的写-读就不能与锁的释放-获取具有相同的内存语义了。为了提供一种比锁更轻量级的线程间的通信机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序。
编译器还好说,JVM是怎么还能限制处理器的重排序的呢?它是通过内存屏障来实现的。
什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。
注意这里的缓存主要指的是CPU缓存,如L1,L2等
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何程序中都能得到正确的volatile内存语义。这个策略是:
- 在每个volatile写操作前插入一个StoreStore屏障;
- 在每个volatile写操作后插入一个StoreLoad屏障;
- 在每个volatile读操作后插入一个LoadLoad屏障;
- 在每个volatile读操作后再插入一个LoadStore屏障。
大概示意图是这个样子:
再逐个解释一下这几个屏障。注:下述Load代表读操作,Store代表写操作
java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
/**
* 现在已经将isOver设置成了volatile变量,这样在main线程中将isOver改为了true后,thread的工作内存该变量值就会失效,从而需要再次从主内存中读取该值,现在能够读出isOver最新值为true从而能够结束在thread里的死循环,从而能够顺利停止掉thread线程。
*/
private static volatile boolean isOver = false;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!isOver){
};
}
});
thread.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
isOver = true;
}
现在已经将isOver设置成了volatile变量,这样在main线程中将isOver改为了true后,thread的工作内存该变量值就会失效,从而需要再次从主内存中读取该值,现在能够读出isOver最新值为true从而能够结束在thread里的死循环,从而能够顺利停止掉thread线程。
3. volatile 用途
从volatile的内存语义来看,可以保证内存可见性禁止重排序
在保证内存可见性这一点上,volatile有着和锁相同的内存语义,可以作为一个轻量级锁来使用。但是由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁可以保证整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在性能上volatile更有优势。
在禁止排序上,volatile非常有用,常见的单例模式,其中有一种实现方式是“双重锁检查”
public class Singleton{
private static volatile Singleton singleton;//不适用volatile关键字 -1
//双重锁检查
public static Singleton getInstance(){
if (singleton == null){// 2
synchronized (Singleton.class){
if (singleton == null){
singleton = new Singleton();//3
}
}
}
return singleton;
}
}
如果1不适用volatile,会重排序出问题变成 1-3-2。