Skip to content

在 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 层面有锁但对代码不可见,面试时主动讲清这层语义,面试官会认可你理解到位。