- 当程序中某个类只需要存在一个对象实例时,构造方法私有化,提供对应的取得对象的静态方法。
- 或者需要采用延迟初始化来降低初始化类和创建对象的开销,只有在使用这些对象时才进行初始化。
比如,下面是非线程安全的延迟初始化对象的示例代码。
public static Instance getInstance() {
if (instance == null) {// 1:A线程执行
instance = new Instance(); // 2:B线程执行
}
return instance;
}
假设A线程执行代码1的同时,B线程正准备执行代码2。线程A看到的instance为null,那么就会再次进行初始化。
解决方案一:同步处理
public synchronized static Instance getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
由于对getInstance()方法做了同步处理,synchronized将导致性能开销。如果方法被多个线程频繁的调用该方法获取,性能将会下降。
解决方案二:双重检查锁定
下面是使用双重检查锁定来实现延迟初始化的示例代码。
public static Instance getInstance() { // 1
if (instance == null) { // 2:第一次检查
synchronized (DoubleCheckedLocking.class) { // 3:加锁
if (instance == null) // 4:第二次检查
instance = new Instance(); // 5:问题的根源出在这里
} // 6
} // 7
return instance; // 8
}
- 第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。可以大幅降低synchronized带来的性能开销
- 多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
- 这里说一下为什么需要第二次检查,第一次检查null时可能有多个线程都检测到instancce为null准备竞争锁。第二次判断就可以解决线程在竞争到锁后再去初始化。
- 在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
但是这段代码有问题
在线程执行到第2行,判断instance是否为null时,instance引用的对象有可能还没有完成初始,只是正在初始化,分配了内存空间,instance指向了内存空间,那么这个时候instance就不会为null;但是 instance还未初始化好。
所以返回的instance还是未初始化好的对象
问题的根源
第五行代码:instance = new Instance();一共需要进行以下三个步骤
// 1:分配对象的内存空间
// 2:初始化对象
// 3:设置instance指向刚分配的内存地址
伪代码中的2和3之间,可能会指令会被重排序。也就造成了分配了内存空间,instance指向了内存空间,那么这个时候instance就不会为null;但是 instance还未初始化好
解决重排序这个问题:不允许2和3重排序。
把instance声明为volatile型
private volatile static Instance instance;
public static Instance getInstance() { // 1
if (instance == null) { // 2:第一次检查
synchronized (DoubleCheckedLocking.class) { // 3:加锁
if (instance == null) // 4:第二次检查
instance = new Instance(); // 5:问题的根源出在这里
} // 6
} // 7
return instance; // 8
}
当声明对象的引用为volatile后,伪代码中的2和3之间的重排序,在多线程环境中将会被禁止重排序。当第一个操作为普通读写时,第二个操作写volatile变量读写时会禁止操作之间的重排序