在 Java 中,不使用锁如何实现一个线程安全的单例?
- 标签:Java、后端、场景题
🏛 业务架构师
结论:不加锁实现线程安全单例,靠的是 JVM 类加载机制本身的保证,两种标准写法:
方式一:静态内部类(Holder 模式,推荐)
java
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 首次访问才触发类加载
}
}原理:JVM 保证类初始化是线程安全的(类初始化锁),外部类加载时 Holder 不会被加载,第一次调用 getInstance() 才触发,天然实现懒加载 + 线程安全,代码零锁零 volatile。
方式二:枚举
java
public enum Singleton {
INSTANCE;
}《Effective Java》推荐,额外防反射攻击和反序列化破坏单例,写法最少。
顺带对比 DCL:双重检查锁需要 volatile 修饰实例字段(防止指令重排导致别的线程拿到"半初始化对象"),能用但容易写错,属于"要懂但不推荐手写"的方案。
架构师视角我一定会补的边界:这些方案只解决单 JVM 内的单例。一旦应用多副本部署(生产标配),每个实例各有一个"单例",全局唯一性就破了——这是面试官埋的下一个坑,也是真实业务里踩过的坑:定时任务部署了两副本就重复发短信、重复对账。真要集群级全局唯一,就得上数据库唯一约束抢占或分布式选主,并且资损类动作仍然要用业务幂等兜底。能把"JVM 单例"和"分布式单例"的问题域区分开,才是这道题的完整答案。
🏗 系统架构师
不用锁(不加 synchronized/Lock),Java 里有三条成熟路径,我会按可靠性排序作答。
方案一:静态内部类(我最推荐,依赖 JVM 类加载机制)
java
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 首次访问触发 Holder 初始化
}
}JVM 规范保证类初始化是线程安全的(<clinit> 由 JVM 加锁执行且只执行一次),同时实现了懒加载:不调用 getInstance 就不初始化 Holder。严格说这是借了 JVM 的隐式初始化锁,但代码层面零锁、零 volatile,最简洁。
方案二:CAS 自旋(真·无锁,但要处理竞争失败)
java
private static final AtomicReference<Singleton> REF = new AtomicReference<>();
public static Singleton get() {
for (;;) {
Singleton cur = REF.get();
if (cur != null) return cur;
if (REF.compareAndSet(null, new Singleton())) return REF.get();
}
}无锁无阻塞,但高并发下可能创建多个临时对象(只有一个胜出),且自旋耗 CPU,只适合低频初始化。
方案三:枚举
java
public enum Singleton { INSTANCE; }同样由类加载保证唯一性,还天然防反射攻击、防反序列化破坏, Effective Java 的首选。
架构视角补充:单机单例到了集群就失效,需要升级为"分布式单例"(注册中心选主/租约锁);另外单例常被滥用成全局状态,我会优先评估是否用依赖注入管理生命周期,而不是到处 getInstance。
💻 全栈开发专家
不使用锁(synchronized/Lock),核心思路是借助 JVM 类加载机制或 CAS 的原子性。
方案一:静态内部类(推荐,面试标准答案)
java
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}原理:Holder 类只在第一次 getInstance() 时初始化,JVM 类初始化由类加载器保证互斥且仅一次,这不是显式锁但借用了 JVM 底层的锁机制,还天然支持懒加载。
方案二:CAS(AtomicReference)
java
private static final AtomicReference<Singleton> REF = new AtomicReference<>();
public static Singleton getInstance() {
Singleton s = REF.get();
if (s == null) {
s = new Singleton();
if (!REF.compareAndSet(null, s)) {
s = REF.get(); // 竞争失败取别人放进去的
}
}
return s;
}无锁但竞争激烈时会重复创建对象再丢弃,高并发下有浪费,要说明这个缺点。
补充 最简单的其实是枚举:enum Singleton { INSTANCE; },类加载保证唯一且防反射破坏,Effective Java 首选。
再补一句区分度:volatile 双检锁那种属于"用了锁";类加载方案严格说 JVM 层面有锁但对代码不可见,面试时主动讲清这层语义,面试官会认可你理解到位。