以著名的 double-checked locking 单例模式为例
问题分析
/**
* dcl double checked locking 问题
*/
public class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton(); //t1
}
}
}
return INSTANCE;
}
}
以上的实现特点是:
-
懒惰实例化
-
首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
-
有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE变量,是在同步块之外但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:
其中 -
17 表示创建对象,将对象引用入栈 // new Singleton
-
20 表示复制一份对象引用 // 引用地址
-
21 表示利用一个对象引用,调用构造方法
-
24 表示利用一个对象引用,赋值给 static INSTANCE
也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
关键在于 0: g e t s t a t i c 这 行 代 码 在 m o n i t o r 控 制 之 外 \color{red} getstatic 这行代码在 monitor 控制之外 getstatic这行代码在monitor控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取INSTANCE 变量的值
也就是
发
生
了
指
令
重
排
序
将
I
N
S
T
A
N
C
E
赋
值
操
作
放
在
了
S
i
n
g
l
e
t
o
n
初
始
化
之
前
\color{red} 发生了指令重排序 将INSTANCE赋值操作放在了Singleton初始化之前
发生了指令重排序将INSTANCE赋值操作放在了Singleton初始化之前
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例
要想保证t2线程获取到的是一个初始化完毕的单利, 对 I N S T A N C E 使 用 v o l a t i l e 修 饰 即 可 , 可 以 禁 用 指 令 重 排 \color{red} 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排 对INSTANCE使用volatile修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效
问题解决
/**
* dcl double checked locking 问题
*/
public class Singleton {
private Singleton() {
}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton(); //t1
}
}
}
return INSTANCE;
}
}
字节码上看不出来 volatile 指令的效果
Compiled from "Singleton.java"
public class com.sunfeng.n5.Singleton {
public static com.sunfeng.n5.Singleton getInstance();
Code:
// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:Lcom/sunfeng/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class com/sunfeng/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:Lcom/sunfeng/n5/Singleton;
14: ifnonnull 27
17: new #3 // class com/sunfeng/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcom/sunfeng/n5/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcom/sunfeng/n5/Singleton;
40: areturn
Exception table:
from to target type
11 29 32 any
32 35 32 any
static {};
Code:
0: aconst_null
1: putstatic #2 // Field INSTANCE:Lcom/sunfeng/n5/Singleton;
4: return
}
如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:
- 可见性
- 写屏障保证在该屏障之前的,t1线程对共享变量的改动,都已经写入内存
- 读屏障保证在该屏障之后的,t2线程对共享的读取读取,加载的都是主内存中的数据
- 有序性
- 写屏障在保证指令重排序时,会保证在写屏障之前的操作排在屏障之前
- 读屏障在保证指令重排序时,会保证在读屏障之后的操作排在屏障之后
- 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性
参考 https://www.bilibili.com/video/BV16J411h7Rd?p=149&spm_id_from=pageDriver