Skip to content

单例模式有哪几种实现?如何保证线程安全? ​

  • 标签:设计模式

🏛 业务架构师 ​

常见实现与适用条件:

  • 饿汉式:类加载即创建。最简单、线程安全由类加载机制保证,没有特殊理由它就是默认选择。缺点是不管用不用都占内存(一般无所谓);
  • 懒汉式 + synchronized:延迟创建但每次获取都竞争锁——不要用,性能损失换不来收益;
  • 双重检查锁(DCL):懒加载 + 高效。必须 volatile 修饰实例引用——防止"对象未初始化完引用已可见"的指令重排(拿到半成品对象是真实事故)。写对了能用,但细节多、易错;
  • 静态内部类:懒加载(内部类首次使用才加载)+ 线程安全(类加载机制),是"要懒加载"时的最优解——DCL 的效果没有 DCL 的坑;
  • 枚举:《Effective Java》推荐——天然防反射攻击与反序列化破坏单例,工具类的单例(序列化敏感的)首选。

业务视角我要拔高两点:

  1. Spring 时代,手写单例的需求大幅萎缩:bean 默认单例、容器管理生命周期,业务对象交给 IOC 是主流——手写单例主要剩框架/工具类场景(无容器环境的 SDK、配置管理器);
  2. 单例的真正纪律是"无状态":单例 = 全局共享,一旦带可变成员变量就是并发事故之源(多个请求读写同一个字段串数据)。我 CR 时看到单例里的可变状态一律要求移除或改为 ThreadLocal/并发容器。

一句话:实现选型是小事(默认饿汉、要懒就静态内部类),"单例必须无状态"才是业务正确性的大事。

🏗 系统架构师 ​

按线程安全实现强度递进讲:

  • 饿汉式:类加载即创建(static final 实例),JVM 类加载机制保证线程安全,简单可靠,缺点是不管用不用都占内存、构造有副作用时不可控。变体:静态代码块、枚举(《Effective Java》推荐——防反射攻击 + 防反序列化破坏);
  • 懒汉式 + synchronized:方法级同步,第一次用才建,但每次调用都竞争锁,性能差,实际淘汰;
  • 双重检查锁 DCL:synchronized + volatile,经典形态。volatile 不可省:new 分三步(分配内存 → 初始化 → 赋引用),无 volatile 时 2、3 可能重排,另一线程拿到"非 null 但未初始化完"的引用——这是并发里最著名的坑;
  • 静态内部类:private static class Holder { static final Instance = new ...; }——利用类加载的线程安全 + 内部类懒加载,兼具懒加载与无锁,是我日常首选;
  • ThreadLocal / 容器托管:Spring Bean 天然容器级单例,业务上 99% 单例直接 @Component。

我补两个工程视角:单例模式在全链路压力下可能成为隐藏共享状态热点(无状态单例才安全);真要全局唯一(分布式环境),单例类不够,要靠 DB 唯一约束/分布式锁保证——JVM 单例的"唯一"边界只是一个进程。

💻 全栈开发专家 ​

四种主要实现,按线程安全强度讲:

1. 饿汉式(简单可靠)

java
public class ConfigHolder {
    private static final ConfigHolder INSTANCE = new ConfigHolder();  // 类加载时创建
    private ConfigHolder() {}
    public static ConfigHolder getInstance() { return INSTANCE; }
}

类加载由 JVM 加锁保证线程安全。缺点:不管用不用都创建(一般无所谓)。

2. 双检锁 DCL(懒加载经典,必须 volatile)

java
private static volatile ConfigHolder instance;   // volatile 防止重排序!
public static ConfigHolder getInstance() {
    if (instance == null) {                       // 第一检:避免不必要加锁
        synchronized (ConfigHolder.class) {
            if (instance == null) {               // 第二检:防止重复创建
                instance = new ConfigHolder();    // 分配→初始化→赋值可能重排
            }
        }
    }
    return instance;
}

没有 volatile 会怎样(面试题眼):new 的三步重排成"先赋引用再初始化",另一线程第一检拿到未初始化完的对象。

3. 静态内部类(推荐,兼顾懒加载与安全)

java
private static class Holder { static final ConfigHolder INSTANCE = new ConfigHolder(); }
public static ConfigHolder getInstance() { return Holder.INSTANCE; }  // 首次使用才加载

4. 枚举(Effective Java 推荐):天然防反射攻击和反序列化破坏单例,适合需要序列化的场景。

我的实践:在 Spring 环境里单例交给容器(默认 scope),手写单例只剩工具类/驱动类这些场景,首选静态内部类;能被"面试官追问 volatile"的只有 DCL,务必讲透。