并发5️⃣内存②volatile 原理、happens-before 规则
1、volatile 原理(❗)
volatile
可以保证可见性、有序性。
- 原理:内存屏障(Memory Barrier / Memory Fence)
- 应用:单例模式-双重检查锁(double-checked locking, DCL)
1.1、内存屏障
volatile 修饰的变量决定了内存屏障的位置。
屏障前后的任意共享变量都会根据规则生效。
写屏障(sfence) | 读屏障(lfence) | |
---|---|---|
位置 | 在 volatile 变量的写指令之后 | 在 volatile 变量的读指令之前 |
可见性保证 | 写屏障之前对任意共享变量的改动,都会同步到主存中 | 读屏障之后对任意共享变量的读取,是主存中的最新值 |
有序性保证 | 写屏障之前的代码不会被重排序到写屏障中 | 读屏障之后的代码不会被重排序到读屏障之前 |
1.2、DCL
1.2.1、实现
public class DclSingleton {
private static volatile DclSingleton instance;
private DclSingleton() {}
public DclSingleton getInstance() {
// 第一次检查
if (instance == null) {
synchronized (DclSingleton.class) {
// 第二次检查
if (instance == null) {
instance = new DclSingleton();
}
}
}
return instance;
}
}
1.2.2、并发度分析
初次访问 getInstance()
- 单线程访问 getInstance()
- 一次检查通过,加锁。
- 二次检查通过,初始化并返回实例对象。
- 多线程访问 getInstance()
- 线程 t1 先进入方法,一次检查通过,加锁。
- 此时线程 t2 一次检查通过但加锁失败,进入 Monitor 的 EntryList 阻塞。
- 线程 t1 二次检查通过,初始化实例对象,返回实例对象之前释放对象锁并唤醒 t2。
- 线程 t2 加锁,二次检查不通过,直接返回实例对象。
再次访问 getInstance()
无论是单线程还是多线程,第一次检查不通过,说明对象已实例化,直接返回实例对象。
1.2.3、volatile 作用
分析:
getInstance()
中的对象实例化instance = new DclSingleton();
对应 4 行字节码
-
new:创建对象实例,分配堆内存,将对象引用压入操作数栈。
-
dup:复制操作数栈的栈顶数据,用于初始化(参考字节码技术)
-
invokespecial:初始化
-
putstatic:将对象引用赋值给变量 instance。
new #2 dup invokespecial #3 putstatic #4
指令重排:假设以上代码没有
volatile
JVM 运行期优化,可能将
invokespecial
和putstatic
顺序对调。
- 首次调用
getInstance()
,执行putstatic
时- 对象尚未初始化。
- 此时 instance 被赋值成一个未初始化的对象(非空)。
- 再次调用
getInstance()
时- 一次检查不通过,直接返回 instance。
- 但此时的 instance 尚未初始化完毕(❗)
2、happens-before
happens-before:可见性和有序性的规则总结。
规定了对共享变量的写操作对其它线程的读操作可见。
写 | 写之后谁可见? |
---|---|
t1 对 lock 加锁期间的写 | 对 lock 加锁的线程 |
t1 对 volatile 变量 x 的写 | 读取 x 的线程 |
t1 启动(start)前的写 | t1 |
t1 终止(死亡)前的写 | 得知 t1 终止的线程 如 t1.isAlive() 、t1.join() |
t1 被中断(interupt)前的写 | 得知 t1 被中断的线程 如 t1.interrupted() 、t1.isInterrupted() |
注:传递性
- 若 t2 可见 t1 的写操作,t3 可知 t2 的写操作,则 t3 可知 t1 的写操作。
- 即
t1 → t2
、t2 → t3
👉t1 → t3