1. 引言
本文将探讨双重检查锁定(Double-Checked Locking)设计模式。这种模式通过预先检查锁定条件来减少锁获取次数,从而提升性能。但需要注意:在Java 5之前,双重检查锁定的实现存在缺陷。下面我们深入分析其工作原理。
2. 实现方式
首先看一个采用严格同步的简单单例:
public class DraconianSingleton {
private static DraconianSingleton instance;
public static synchronized DraconianSingleton getInstance() {
if (instance == null) {
instance = new DraconianSingleton();
}
return instance;
}
// 私有构造器和其他方法...
}
尽管这个类是线程安全的,但存在明显的性能缺陷:每次获取单例实例时都需要获取锁,即使这个锁可能完全没必要。
要解决这个问题,我们可以先检查是否需要创建对象,仅当需要时才获取锁。更进一步,在进入同步块后需要再次执行相同检查,以保证操作的原子性:
public class DclSingleton {
private static volatile DclSingleton instance;
public static DclSingleton getInstance() {
if (instance == null) {
synchronized (DclSingleton.class) {
if (instance == null) {
instance = new DclSingleton();
}
}
}
return instance;
}
// 私有构造器和其他方法...
}
⚠️ 使用此模式时需注意:**字段必须声明为volatile
**,否则可能导致缓存不一致问题。Java内存模型允许发布部分初始化的对象,这会引发难以察觉的bug。
3. 替代方案
尽管双重检查锁定可能提升性能,但它存在两个明显问题:
- 需要
volatile
关键字,与Java 1.4及以下版本不兼容 - 代码冗长,可读性差
基于这些原因,我们来看几种没有这些缺陷的替代方案。以下方法都将同步任务委托给JVM处理。
3.1. 饿汉式初始化
实现线程安全最简单的方式是内联对象创建或使用等效的静态代码块。这利用了静态字段和块按顺序初始化的特性(Java语言规范12.4.2):
public class EarlyInitSingleton {
private static final EarlyInitSingleton INSTANCE = new EarlyInitSingleton();
public static EarlyInitSingleton getInstance() {
return INSTANCE;
}
// 私有构造器和其他方法...
}
3.2. 按需初始化
根据前文提到的Java语言规范,类初始化发生在首次使用其方法或字段时。我们可以利用嵌套静态类实现延迟初始化:
public class InitOnDemandSingleton {
private static class InstanceHolder {
private static final InitOnDemandSingleton INSTANCE = new InitOnDemandSingleton();
}
public static InitOnDemandSingleton getInstance() {
return InstanceHolder.INSTANCE;
}
// 私有构造器和其他方法...
}
这种方案中,InstanceHolder
类会在首次调用getInstance()
时初始化字段。
3.3. 枚举单例
最后一种方案来自Joshua Bloch的《Effective Java》(第3条),使用enum
替代类。截至目前,这被认为是最简洁安全的单例实现方式:
public enum EnumSingleton {
INSTANCE;
// 其他方法...
}
4. 总结
✅ 本文快速梳理了双重检查锁定模式、其局限性及替代方案。
实际开发中,双重检查锁定的冗长代码和向后兼容性问题使其容易出错,应尽量避免使用。推荐采用让JVM处理同步的替代方案。
所有示例代码可在GitHub获取。