开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第32天,点击查看活动详情
1.双重检查锁示例代码
示例代码如下:
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLocking.class) {
if (instance == null) {
instance = new Instance;
}
}
}
return instance;
}
}
截图如下:
前面的双重检查锁其中第七行的instance = new Instance;这个赋值语句,在字节码指令中其实是三步骤:
memory = allocate(); // A:分配对象的内存空间
ctorInstance(memory); // B:初始化对象
instance = memory; // ,C:设置instance指向刚分配的内存地址
上面赋值语句主要是拆分为三步:A:分配对象的内存空间,B:初始化对象,C:设置instance指向刚分配的内存地址。 如果此时发生指令重排序,那么有可能会执行顺序变成ACB,这样当线程1走到第7步ACB中的C步骤时,此时线程2也进来,假设线程2走到第4步时进行instance == null判断发现已经有对象了(因为线程1执行完C步骤时已经产生对象但是还没进行初始化),但是这时候对象还没有初始化,线程2直接拿着这个空的(有内存地址,但是里面没东西)instance返回了。
2.解决方法1
public class DoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLocking.class) {
if (instance == null) {
instance = new Instance;
}
}
}
return instance;
}
}
只是在变量定义的时候多加了一个volatile关键字,可以禁止指令重排序。其实里面的禁止指令重排序真正的是在ABC这3步前面加一个storestore屏障,在ABC这3步后加一个storeload屏障。
因为new instance 他是一个 JVM 指令码,对应的是 new 指令。 Volatile能够保障单个JVM 指令的原子性,所以此处, new instace相当于是volatiole写,会在 new instance前加 storestore,后加storeload屏障,线程2就必须在storeload 屏障后边读取。(不建议面试使用。)实质上,new对象里边的三个小步骤,依然可以重排序,真正的控制是在外层的内存屏障控制。
3.解决方法2
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
public class InstanceFactory {
private static class InstanceHolder{
public static Instance instance = new Instance();
}
public static Instance getInstance() {
// 这里将导致InstanceHolder类被初始化
return InstanceHolder.instance ;
}
}