目录,
Q:为什么会出现延迟初始化
1.以下这段代码,如果有多个线程竞争这个锁的话,将会有很大的开销
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
}
为了降低锁带来的性能损耗,所以使用了双重检查锁(double-check locking),如下代码所示
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) { //4:第一次检查
synchronized (DoubleCheckedLocking.class) { //5:加锁
if (instance == null) //6:第二次检查
instance = new Instance(); //7:问题的根源出在这里
}
}
return instance;
}
}
在第一次检查的时候,另外一个线程访问到instance变量不空,不需要等待获得锁,大大降低了锁的损耗。
Q:延迟初始化带来的并发错误是什么?
但这个是错误的优化,另外一个线程当程序执行到第4行代码的时候,判断instance不为空的时候,程序还没有完成初始化。
一个对象要想初始化需要经历三步:
instance = new Instance();
这行的代码可由三部分构成,
// 1.分配内存空间
alloctMemory(instance);
// 2.初始化对象
initial(instance);
// 3.设置instance指向刚分配的内存地址
instance = memory
因为2、3会因为指令重排序的原因带来错误,所以需要优化这个问题。
只要保证2在4的前面,就不会有问题,初次访问对象的守候,必须已经初始化完对象。
A:有两种解决方案
解决方案1.使用volatile禁止重排序
解决方案(1)是禁止重排序,这个办法就是使用volatile修饰变量
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance();//instance为volatile,现在没问题了
}
}
return instance;
}
}
解决方案2.基于类的初始化的同步机制
解决方案(2)运行该线程重排序,但是不允许其他线程看到这个重排序的过程。
jvm在类的初始化阶段,会先加载class对象到内存中,然后使用jvm加锁同步多个线程对类的初始化。
public class InstanceFactory {
// 使用静态内部类
private static class InstanceHolder {
public static Instance instance = new Instance();
}
// 调用静态内部类的方法
public static Instance getInstance() {
return InstanceHolder.instance ; //这里将导致InstanceHolder类被初始化
}
}
这种方式是直接对类的初始化上了锁,所以线程B肯定看不到重排序
那么可以用这个思路,将双重检查的代码写到这个用进行类的初始化的代码里面。
(扩展)那么哪时候会进行类的初始化呢?
(详解)类初始化中的同步处理机制
总共分为4个阶段:
第一阶段(竞争获得初始化锁)
在class对象上同步,即获取class对象的初始化锁,来初始化类和接口,这个获取锁的线程会一直等待,直到获取锁。
Class A = classLoader.loadClass(InstanceHolder.class);
如果线程A和B同时竞争初始化Class对象A
第二阶段(进行类的初始化和静态变量的初始化)
当A获得初始化锁的时候,A将会进行类的初始化(如果是静态类,那么将进行静态类的初始化)和静态字段的初始化,B将获得初始化锁,检查锁的状态
第三阶段:线程A设置class的state为initialized,并唤醒锁中condition中的所有线程,释放持久化锁
第四阶段:线程B结束类的初始化处理过程
线程A在第二阶段A1进行类的初始化,并且在第三阶段A4阶段释放锁后,线程B才获得初始化锁,这里存在着happen-before关系
※注1:这里的condition和state标记是虚构出来的。JVM的具体实现只要实现类似功能即可。
(说明)解决方案2的使用场景
解决方案2的使用场景:每次类执行的时候,首先会初始化锁,然后读取锁的state状态,如果锁的state状态时initialized,就会释放该锁,直接调用方法。如果类已经初始化完成,那如果有两个线程同时访问延迟初始化的代码,还是会存在重排序导致的错误问题。
所以使用类初始化解决延迟初始化带来的问题只适用于类还没有初始化的时候。