双重检查锁定是一种用于减少同步开销的设计模式,主要用于单例模式的延迟初始化。下面我将详细分析这种模式在Java中的实现、问题及解决方案。
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;
}
}
上述实现看似正确,但实际上在Java 1.4及之前版本中存在严重问题。问题出在instance = new Singleton()
这一行,它不是一个原子操作,实际包含三个步骤:
由于指令重排序优化,可能导致2和3的顺序颠倒,即引用已经指向了内存地址但对象尚未初始化完成。这会导致其他线程可能获取到一个未完全初始化的对象。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile
关键字可以防止指令重排序,确保对象的完全初始化对其他线程可见。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式利用了类加载机制保证线程安全,且实现了延迟加载。
public enum Singleton {
INSTANCE;
public void someMethod() {
// 方法实现
}
}
枚举方式由JVM保证线程安全和单一实例,是最简洁安全的单例实现方式。
双重检查锁定在正确实现后确实比简单同步方法有性能优势: - 只有在第一次创建实例时需要同步 - 后续访问完全不需要同步开销
在Java 8+中,可以考虑使用java.util.concurrent
包中的原子类:
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
private Singleton() {}
public static Singleton getInstance() {
Singleton instance = INSTANCE.get();
if (instance == null) {
instance = new Singleton();
if (INSTANCE.compareAndSet(null, instance)) {
return instance;
} else {
return INSTANCE.get();
}
}
return instance;
}
}
volatile
修饰符的正确双重检查锁定是可行的选择哪种方式取决于具体需求和Java版本,但通常推荐使用枚举或静态内部类方式实现单例模式。