单例设计模式要求一个类在整个应用生命周期中只能存在一个实例,双重检查锁是兼顾性能与线程安全的常用实现方式,不过其中静态变量的处理不当很容易引发安全性问题,需要针对性优化。

双重检查锁的基础实现与问题
最基础的不加优化的双重检查锁单例实现如下,看似线程安全,实际存在隐患:
public class Singleton {
// 静态变量存储单例实例
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次检查,避免不必要的同步
if (instance == null) {
synchronized (Singleton.class) {
// 第二次检查,确保只有一个实例被创建
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上述代码的问题在于instance = new Singleton()这行代码不是原子操作,它实际分为三个步骤:分配内存空间、初始化对象、将instance指向分配的内存空间。如果发生了指令重排,步骤2和步骤3的顺序可能互换,此时另一个线程在第一次检查时发现instance不为null,就会直接返回还未完成初始化的对象,导致使用时出现空指针异常。
静态变量的安全性优化方案
要解决指令重排的问题,只需要给静态变量添加volatile关键字修饰,禁止指令重排即可,优化后的完整代码如下:
public class SafeSingleton {
// 用volatile修饰静态变量,禁止指令重排
private static volatile SafeSingleton instance;
private SafeSingleton() {}
public static SafeSingleton getInstance() {
// 第一次检查,未初始化时才进入同步块
if (instance == null) {
synchronized (SafeSingleton.class) {
// 第二次检查,确保只有一个实例被创建
if (instance == null) {
instance = new SafeSingleton();
}
}
}
return instance;
}
}
优化原理说明
volatile关键字的作用有两个:一是保证变量的可见性,一个线程修改了变量的值,其他线程能立刻感知到;二是禁止指令重排,保证instance = new SafeSingleton()的三个步骤按顺序执行,避免出现未初始化完成的对象被其他线程访问的情况。
方案对比
以下是优化前后的方案对比:
| 方案类型 | 线程安全 | 是否存在指令重排风险 | 性能表现 |
|---|---|---|---|
| 基础双重检查锁 | 否 | 是 | 较好 |
| volatile优化后双重检查锁 | 是 | 否 | 较好 |
| 静态内部类单例 | 是 | 否 | 好 |
其他注意事项
- 单例类的构造方法必须私有化,防止外部通过new关键字创建实例。
- 如果单例类需要序列化,要添加
readResolve方法,避免反序列化时创建新的实例。 - 在分布式场景下,双重检查锁的单例仅对单个JVM实例有效,跨JVM需要额外的处理。
双重检查锁的优化核心就是处理好静态变量的可见性和指令重排问题,添加volatile修饰是最直接有效的方案,实际开发中可以根据场景选择使用。