前言
在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。
第一种(存在问题)
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) // 1:A线程执行
instance = new Instance(); // 2:B线程执行
return instance;
}
}
假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化。(对象初始化可能发生重排序,单线程无影响,多线程可能出现问题)
创建对象
创建对象三部曲(正确顺序):
1、分配对象的内存空间
2、初始化对象
3、设置 instance 指向刚分配的内存地址
步骤2、3可能重排序,重排序后:
此时,这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能(因此可能发生)。
然而多线程情况下,就会出现上述问题。
第二种(改进第一种)
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
}
方法上加个synchronized,使线程串行执行,不会出现问题,但是synchronized锁的代码块太多,锁颗粒太大,导致性能不是特别好。
第三种(DCL)
public class DoubleCheckedLocking {
// volatile 保证创建对象时不会发生重排序
private volatile static Singleton instance;
public static Singleton getInstance() {
// 第一次检查 (代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。)
if (instance == null) {
// synchronized锁更少的代码性能会好一些
synchronized (DoubleCheckedLocking.class) { // 加锁
if (instance == null) // 第二次检查
/*
问题的根源出在这里,对象不加 volatile 修饰,创建对象时可能发生重排序,导致先创建一个空对象,后续在初始化,单线程最后结果不会受到影响所以可以重排序,此时另一个线程第一次检查不为null,直接返回结果但是此时对象还未初始化解决问题只需将 对象由 volatile修饰禁止指令重排即可
*/
instance = new Singleton();
}
}
return instance;
}
}
与方法一类似,由于创建对象可能发生重排序,导致多线程情况下,线程可能拿到未初始化的对象。
两个办法来实现线程安全的延迟初始化:
- 不允许2和3重排序。
- 允许2和3重排序,但不允许其他线程“看到”这个重排序。
为了保证创建对象不会发生重排序,单例对象应该使用volatile修饰。(基于volatile的解决方案)
第四种(基于类初始化的解决方案)
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
// 这里将导致InstanceHolder类被初始化
return InstanceHolder.instance ;
}
}
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁(初始化锁:与类或者接口一一对应)。这个锁可以同步多个线程对同一个类的初始化(保证类只会被初始化一次)。【JVM在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了
】
对于类或接口的初始化,Java语言规范制定了精巧而复杂的类初始化处理过程。(自行探索)
volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。