开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第36天,点击查看活动详情
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 执行单例构造函数");
}
public static SingletonDemo getInstance(){
if(instance == null){
synchronized (SingletonDemo.class){
if(instance == null){
instance = new SingletonDemo(); //pos_1
}
}
}
return instance;
}
pos_1处的代码转换成汇编代码如下
volatile保证可见性原理
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查IA-32架 构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和主内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现MESI缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
注意:lock前缀指令是同时保证可见性和有序性(也就是禁止指令重排)的
注意:lock前缀指令相当于一个内存屏障【后文讲】
volatile禁止指令重排的原理
public class VolatileExample {
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
}
}
}
在JSR-133之前的旧的Java内存模型中,是允许volatile变量与普通变量重排序的。那上面的案例中,可能就会被重排序成下列时序来执行:
- 线程A写volatile变量,step 2,设置flag为true;
- 线程B读同一个volatile,step 3,读取到flag为true;
- 线程B读普通变量,step 4,读取到 a = 0;
- 线程A修改普通变量,step 1,设置 a = 1;
可见,如果volatile变量与普通变量发生了重排序,虽然volatile变量能保证内存可见性,也可能导致普通变量读取错误。
所以在旧的内存模型中,volatile的写-读就不能与锁的释放-获取具有相同的内存语义了。为了提供一种比锁更轻量级的线程间的通信机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序。
编译器还好说,JVM是怎么还能限制处理器的重排序的呢?它是通过内存屏障来实现的。
什么是内存屏障?硬件层面,内存屏障分两种:读屏障(Load Barrier)和写屏障(Store Barrier)。内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。
注意这里的缓存主要指的是上文说的CPU缓存,如L1,L2等