单例模式为什么使用双重检查机制?为什么用volatile修饰单例对象?
- 为什么使用双重检查机制?
-
如果单例已经创建了,直接调用synchronized加锁会比较消耗性能。所以首先判断对象有没有创建,没有创建再加锁。
-
加锁为了只让一个线程去创建对象。第二层非空检查的原因是在同时多个线程调用时,A线程获得锁并创建成功实例,之后释放锁,前面一起竞争的B线程获得锁,首先判断非空,代表已经创建了,所以不会继续去创建实例。
-
为什么单例对象要用volatile修饰?
volatile是为了防止指令重排序带来的多线程问题。
创建一个对象这行代码可以分解成3个步骤:
memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory(); // 3.设施instance指向刚分配的内存地址
在一些JIT编译器上,2和3之间可能会发生重排序:
memory = allocate(); // 1.分配对象的内存空间
instance = memory(); // 3.设施instance指向刚分配的内存地址
ctorInstance(memory); // 2.初始化对象
在重排序后,在多线程中的问题来了:
A线程在创建实例过程中将instance指向内存空间后(步骤3),此时install已经不为null,但实例还未初始化。此时!!! B线程在第一次检查instance是否为null时,为非空状态,此时访问到(返回)的是一个未初始化的对象。
-
基于volatile的解决方案:
禁止分配内存空间(步骤2)和初始化对象(步骤3)之间的指令重排序。避免了访问到未初始化对象的可能性。
public class Singleton {
private Singleton() {
}
private volatile static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
-
基于类初始化的解决方案:
静态内部类在使用的时候才加载,起到了懒加载的作用。
JVM在执行类的初始化期间,会去获取一个锁(初始化锁)。这个锁可以同步多个线程对同一个类的初始化(执行类的静态初始化和初始化类中声明的静态字段,此过程中发生的重排序对其它线程不可见)。由JVM的类加载保证了多线程初始化的并发问题。
public class InstanceFactory {
// 静态内部类持有一个实例
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance () {
return InstanceHolder.instance; // 这里将导致 InstanceHolder 类被初始化
}
}
《并发编程的艺术》
版权声明:本文为qq_40996976原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。