一、简介
volatile 适用于一个线程写,一个线程读的场景。
volatile 的底层实现原理是内存屏障,即 Memory Barrier。针对于 volatile 修饰的变量,当接收到写指令(修改变量值)后会加入写屏障,当接收到读指令(获取变量值)前会加入读屏障。
二、如何保证可见性
写屏障保证在该屏障之前的,对共享变量的修改,都同步到主存当中,如下代码中,ready 是使用 volatile 修饰的。此时就会把 num 和 ready 的最新值都同步到主存中。
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据,如下代码,ready 是使用 volatile 修饰的,当获取 ready 值的时候就会在其之前加入读屏障,保证读取的 ready 的值是从主从中读取的。
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
三、如何保证有序性
写屏障会确保指令重排时,不会将写屏障之前的代码排在写屏障之后,如下代码之中,保证了 num 和 ready 的顺序不会发生变化。
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
四、注意
虽然 volatile 可以保证可见性和有序性,但还是不能解决多线程环境下指令交错的问题(原子性)。即写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去。而有序性的保证也只是保证了本线程内相关代码不被重排序。
五、double-checked locking 问题
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
上面这种实现的特点是:懒惰实例化,首次使用 getInstance() 才使用 synchronized 加锁,后续使用无需加锁。
但是有一个很关键的点:第一个 if 使用了 INSTANCE 变量,是在同步代码块之外。但是在多线程环境下,上面的代码是有问题的。
getInstance() 方法对应的字节码如下所示:
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
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:Lcn/itcast/n5/Singleton;
40: areturn
17 表示创建对象,将对象的引用入栈 new Singleton
20 表示复制一份对象引用
21 表示利用一个对象引用,调用构造方法
24 表示利用一个对象引用,赋值给 static INSTANCE
也许 JVM 会优化为:先执行 24,再执行 21。关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值。
假设此时线程一还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么线程二拿到的是将是一个未初始化完毕的单例,就出现了问题。
六、double-checked locking 解决
对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效,如下代码:
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}