JUC源码解析:深入理解 volatile
volatile 的定义
volatile 的作用:
- 保证可见性
- 禁止指令重排序
volatile 可以被看作是轻量版的 synchronized,volatile 保证了多线程中共享变量的“可见性”,就是说,当volatile 变量的值被修改时,其他线程能读取到被修改的变量值。
如果volatile 使用恰当的话,它比synchronized的执行成本更低,因为volatile 不需要线程上下文的切换,并且在“读多写少”的情况下,volatile的效率更好。
为什么在“读多写少”的情况下,volatile的效率更好?
因为在 volatile 的JMM内存屏障中定义,如果发生对 volatile 变量的读操作,会在 读之后 设置内存屏障,而读之前是没有内存屏障的。在 写前后 却都有内存屏障。所以读多写少的情况下volatile的效率会更高些。
关于JMM内存屏障,请看下一节的介绍
volatile 禁止指令重排序原理
volatile 会在编译器生成字节码时,插入JMM内存屏障来保障 volatile 变量不被重排序。
JMM有四个volatile内存策略:
- 写操作:
- 写之前插入 StroeStore 屏障
- 写之后插入 StoreLoad 屏障
- 读操作:
- 读之后插入 LoadLoad 屏障
- 读之后插入 LoadStore 屏障
这是非常保守的屏障策略,实际中,如果有这样操作:
//伪代码
volatile int a, b;
a = 1; // volatile 写操作
b = 1; // volatile 写操作
两个写操作在一起时,JMM可以不那么保守。因为两个写操作并在一起,可以在中间省略一些屏障,让程序更有效率。
单例模式线程不安全的核心原因
有些单例模式是线程不安全的,为什么,看看下面的代码:
public class Main {
private static Object instance;
public static Object getInstance() {
if (instance == null) { // 第一次检查
synchronized (Main.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Object(); // 问题的根源出现在这里!!!
}
}
}
return instance;
}
}
很显然,这是一个不适应 volatile 的线程不安全的单例模式。为什么非线程安全,核心漏洞在 instance = new Object();
这一行语句上,我们把它拆解为下面三行伪代码:
memory = allocate(); // 1.为对象分配内存空间
ctorInstance(memory); // 2. 初始化对象
instance = memory; // 3. 设置instance指向刚分配的地址
记住,此时的instance是没有volatile关键字的,它是允许被重排序的,在一些多线程情况加,它可能会被重排序成这种情况
memory = allocate(); // 1.为对象分配内存空间
instance = memory; // 3. 设置instance指向刚分配的地址
ctorInstance(memory); // 2. 初始化对象
还没有在内存中初始化对象,就已经分配地址了!
这时候,如果有另外一个线程钻空子,就可能拿到一个分配了内存地址、但还没有真正初始化的对象。
因此,单例模式在多线程环境下,使用 volatile 禁止重排序是必要的!
使用 volatile 优化后的代码:
public class Main {
private static volatile Object instance;
public static Object getInstance() {
if (instance == null) {
synchronized (Main.class) {
if (instance == null) {
instance = new Object(); // 现在不会被重排序了
}
}
}
return instance;
}
}
深层解读:
instance = new Object();
是 volatile 写操作,JMM会在编译期间为它设置内存屏障。在这句代码之前,会加入 storestroe
屏障,在这句代码之后,会加入 storeload
屏障,这样,其他线程就必须在 stroeload
屏障后面读取,就保证了创建对象时的原子性。