京东Java面试被问:如何使用双重检查锁定在 Java 中创建线程安全的单例?
这是一个非常经典且深入的Java面试题。它考察对多线程、JVM内存模型、类加载机制和设计模式的综合理解。下面我将从原理、演进、陷阱和最佳实践四个层面,彻底讲透双重检查锁定(Double-Checked Locking, DCL)单例。
一、 基础原理与演进史
1. 单例的终极目标
-
保证一个类在任何情况下只有一个实例。
-
提供全局访问点。
-
在多线程环境下绝对安全。
2. 方案的演进(从错误到正确)
版本1:天真的懒汉式(线程不安全)
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 线程A和B可能同时通过这行检查
instance = new Singleton(); // 导致创建多个实例
}
return instance;
}
}
版本2:同步方法(线程安全但性能差)
java
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
-
问题:每次调用都加锁,性能瓶颈严重。
版本3:双重检查锁定(DCL)- 错误版本
java
public class Singleton {
private static Singleton instance; // ❌ 没有volatile
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Singleton(); // ❌ 问题在这里!
}
}
}
return instance;
}
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
二、 为什么需要volatile?——核心陷阱解析
问题出在 instance = new Singleton() 这行代码。它不是原子操作,在JVM中实际分三步执行:
java
memory = allocate(); // 1. 分配内存空间 ctorInstance(memory); // 2. 初始化对象 instance = memory; // 3. 将引用指向分配的内存地址
由于指令重排序(编译器和处理器优化),实际执行顺序可能是 1→3→2。
并发问题场景模拟:
text
时间线: 1. 线程A进入synchronized块,执行到 instance = new Singleton() 2. JVM重排序:先执行了步骤3(instance指向内存),但步骤2(初始化)尚未执行 3. 此时instance != null,但对象还未初始化! 4. 线程B调用getInstance(),第一次检查发现instance != null 5. 线程B直接返回了未完全初始化的“半成品”对象! 6. 线程B使用这个对象 → 空指针异常或逻辑错误
正确的DCL版本(JDK 1.5+)
java
public class Singleton {
// ✅ 必须添加volatile关键字
private static volatile Singleton instance;
private Singleton() {
// 防止反射破坏单例
if (instance != null) {
throw new RuntimeException("Use getInstance() method to get the single instance.");
}
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:避免不必要的同步
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查:防止重复创建
instance = new Singleton();
}
}
}
return instance;
}
}
volatile的两大作用:
-
禁止指令重排序:确保
new Singleton()的步骤按 1→2→3 顺序执行。 -
保证可见性:一个线程修改instance后,其他线程能立即看到最新值。
三、 替代方案(更推荐)
虽然DCL正确,但在实际开发中,有更简洁安全的方案。
方案1:静态内部类(Holder模式)- 推荐
java
public class Singleton {
private Singleton() {
// 构造器私有化
}
// 静态内部类在第一次被引用时才加载
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE; // 触发类加载,线程安全地初始化
}
}
优点:
-
懒加载(调用
getInstance()时才初始化) -
线程安全(JVM保证类加载过程的同步)
-
代码简洁,无需同步锁
方案2:枚举单例(最安全)- 《Effective Java》推荐
java
public enum Singleton {
INSTANCE; // 本身就是单例
public void doSomething() {
// 业务方法
System.out.println("Singleton working...");
}
}
// 使用:Singleton.INSTANCE.doSomething();
优点:
-
绝对防止多实例(JVM保证)
-
防止反射攻击
-
防止序列化破坏单例
-
代码极其简洁
四、 面试回答要点与层次
面试时,建议按以下逻辑层次回答:
第一层:基本实现
“我理解DCL单例需要volatile修饰实例变量,并在getInstance方法中进行两次null检查,并在中间加锁。”
第二层:原理解析
“这是因为 new Singleton() 不是原子操作,可能发生指令重排序。如果没有volatile,其他线程可能看到一个未完全初始化的对象。volatile通过禁止重排序和保证可见性来解决这个问题。”
第三层:演进对比
“DCL在JDK 1.5之后才是完全正确的,因为Java内存模型的增强。不过实际开发中,我通常更推荐使用静态内部类或枚举的方式实现单例,它们更简洁且天生线程安全。”
第四层:生产环境考虑
“如果项目中必须用DCL,我会注意:
-
确保使用JDK 1.5+
-
使用volatile
-
考虑防御反射攻击
-
单例对象如果是重量级资源,还需要考虑销毁问题”
五、 完整代码示例(生产级)
java
/**
* 双重检查锁定单例(生产可用版本)
* 包含防御反射攻击和序列化安全的考虑
*/
public class DclSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile DclSingleton instance;
private DclSingleton() {
// 防御反射攻击
if (instance != null) {
throw new IllegalStateException("Instance already exists.");
}
}
public static DclSingleton getInstance() {
DclSingleton result = instance; // 减少对volatile的直接访问(性能优化)
if (result == null) {
synchronized (DclSingleton.class) {
result = instance;
if (result == null) {
instance = result = new DclSingleton();
}
}
}
return result;
}
// 防止序列化破坏单例
protected Object readResolve() {
return getInstance();
}
public void businessMethod() {
System.out.println("Business logic here.");
}
}
关键优化点:
-
使用局部变量
result减少volatile读取次数(Joshua Bloch在《Effective Java》中建议) -
实现
readResolve()防止序列化破坏 -
构造器中检查防止反射攻击
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
总结
-
DCL单例必须使用volatile(在JDK 1.5+)
-
理解指令重排序和内存可见性是关键
-
静态内部类和枚举是更优选择
-
生产环境要考虑反射、序列化等攻击
掌握这个问题的过程,体现了你对并发编程的理解深度和对细节的关注程度,这正是高级Java工程师的核心能力。
更多推荐


所有评论(0)