Yihui’s Blog

在 Java 中,不使用锁如何实现一个线程安全的单例?

日期:2026-07-11
标签:#面试 #八股 #Java #后端 #场景题

一句话答案

优先使用枚举单例,或利用类初始化阶段的线程安全保证实现静态内部类单例;两者都不需要在业务代码中显式加锁。

面试口语版

如果“不使用锁”指不写 synchronized 或 Lock,我会优先用枚举单例。JVM 保证枚举实例只初始化一次,还天然防反射重复创建和反序列化破坏。若要求懒加载,可以使用静态内部类:外部类加载时不初始化 Holder,第一次调用 getInstance() 时才触发 Holder 初始化,而 JVM 的类初始化过程具备线程安全和安全发布保证。

代码示例

public enum Singleton {
    INSTANCE;
}

public final class LazySingleton {
    private LazySingleton() {}

    private static class Holder {
        private static final LazySingleton INSTANCE = new LazySingleton();
    }

    public static LazySingleton getInstance() {
        return Holder.INSTANCE;
    }
}

关键细节

  • volatile + CAS 也能尝试发布实例,但并发失败线程可能重复构造对象;若构造有副作用就不合适。
  • 双重检查锁本身仍使用锁,不符合题目的字面要求。
  • 单例只保证一个 ClassLoader 内唯一;多个 ClassLoader、多个 JVM 或分布式环境并不全局唯一。
  • 单例中若包含可变字段,实例创建安全不代表这些字段的后续操作线程安全。

面试官追问

  1. JVM 为什么能保证类初始化线程安全?
  2. 枚举如何防止反序列化产生新实例?
  3. 多 ClassLoader 下如何保证唯一?
  4. 单例中的可变状态如何保证线程安全?

面试官追问参考答案

1. JVM 为什么能保证类初始化线程安全?

JVM 在执行类或接口的初始化方法 <clinit> 前会对初始化过程做同步,同一个 ClassLoader 下同一类只会由一个线程执行初始化,其他线程等待。初始化完成后,静态字段的写入对随后使用该类的线程可见,因此 Holder 中的实例能安全发布。

2. 枚举如何防止反序列化产生新实例?

Java 序列化对枚举有特殊规则,只记录枚举常量的名称,反序列化通过 Enum.valueOf 返回已有常量,不调用普通对象构造流程。反射 API 也禁止通过构造器创建枚举实例,因此比普通单例更稳健。

3. 多 ClassLoader 下如何保证唯一?

静态字段属于“类定义 + ClassLoader”,不同 ClassLoader 会各自创建实例,单靠语言级单例无法全局唯一。可把类放到共同父加载器,或把唯一性下沉到进程级注册表、操作系统文件锁/端口;跨 JVM 则要使用协调系统或唯一资源约束。

4. 单例中的可变状态如何保证线程安全?

单例只解决实例数量,不解决字段并发访问。可优先使用不可变对象,更新时通过原子引用替换快照;计数用原子类,复合状态用锁或线程安全容器,并明确可见性和原子性。

学习清单

  • 掌握枚举、饿汉式、静态内部类和双重检查四种写法。
  • 理解类初始化锁、安全发布及 ClassLoader 边界。
Maintained by · YihuiEdit on GitHub

Keep reading

View all posts