单例模式有几种写法?

#中间件#面试八股

单例模式有几种写法?

结论先行:常见 5 种——饿汉、懒汉(含同步)、双重检查锁(Double-Checked Locking)、静态内部类、枚举,另有一种"容器登记"的工程用法。首选枚举(最安全),次选静态内部类(可读性好、真懒加载),DCL 必须记得加 volatile。

1. 先看结构:对外只露一个口子

┌───────────────────────────┐
│  Singleton                │
│  - instance: Singleton    │  ← 私有静态实例
│  - Singleton()            │  ← 私有构造,外部 new 不了
│  + getInstance()          │  ← 唯一访问入口
└─────────────▲─────────────┘
              └── ServiceA / ServiceB / ServiceC 都从这里取

要点就三条:私有构造器、私有静态实例、公开静态获取方法。

2. 五种写法横向对比

写法懒加载线程安全防反射防反序列化备注
饿汉(静态常量)否是(类初始化保证)否否最简单,可能白造对象
懒汉 + synchronized是是否否每次调用都加锁,性能差
双重检查锁是是(需 volatile)否否写法复杂,易漏 volatile
静态内部类是是否否兼顾懒加载与简单
枚举否是是是JVM 保证,最佳实践

3. 饿汉与懒汉

// 饿汉:类加载时就建好,天然线程安全
public class ConfigHolder {
    private static final ConfigHolder INSTANCE = new ConfigHolder();
    private ConfigHolder() { }
    public static ConfigHolder getInstance() { return INSTANCE; }
}

// 懒汉:把 INSTANCE 挪进 static synchronized 方法里延迟创建,代价是每次都抢锁

饿汉的"浪费"其实很小:只是多一个对象和一点初始化时间。真正需要懒加载的场景,是构造过程很重(读大文件、连远端)时才值得。

4. 双重检查锁(DCL)

public class DclSingleton {
    // volatile 关键:禁止"分配内存 → 赋值引用"与"初始化对象"重排序
    private static volatile DclSingleton instance;

    private DclSingleton() { }

    public static DclSingleton getInstance() {
        if (instance == null) {                     // 第一次检查:命中时零加锁
            synchronized (DclSingleton.class) {
                if (instance == null) {             // 第二次检查:防重复创建
                    instance = new DclSingleton();
                }
            }
        }
        return instance;
    }
}

少写 volatile 的后果:另一线程可能拿到引用非空但字段还是默认值的半初始化对象。

5. 静态内部类:延迟加载 + 零同步

public class InnerClassSingleton {
    private InnerClassSingleton() { }

    // 只有第一次调用 getInstance 时才会触发 Holder 的类初始化
    private static class Holder {
        private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
    }

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

依赖 JVM 的类初始化机制(<clinit> 由虚拟机加锁,且只执行一次),所以既懒加载又不用自己加锁。

6. 反射与反序列化会破坏单例

// 反射绕过私有构造
Constructor<InnerClassSingleton> c = InnerClassSingleton.class.getDeclaredConstructor();
c.setAccessible(true);
InnerClassSingleton evil = c.newInstance();   // 拿到第二个实例

// 反序列化同样会 new 出一个新对象,绕过私有构造器

防护手段:构造器里判断实例已存在就抛异常;实现 readResolve() 返回已有实例。而枚举天然免疫:

public enum EnumSingleton {
    INSTANCE;                       // JVM 保证每个枚举常量只实例化一次

    public void doSomething() { /* 业务方法 */ }
}
// 调用:EnumSingleton.INSTANCE.doSomething();

《Effective Java》的结论:单元素枚举是实现单例的最佳方式——JVM 保证每个枚举常量只实例化一次,且反射与反序列化都被专门处理。

7. 容器式单例(Spring 的做法)

public class SingletonRegistry {
    private static final Map<String, Object> BEANS = new ConcurrentHashMap<>();

    public static Object get(String name) {
        return BEANS.computeIfAbsent(name, k -> create(k));   // 不存在才创建
    }

    private static Object create(String name) { /* 反射创建或读配置 */ return null; }
}

Spring 的 DefaultSingletonBeanRegistry 就是"三级缓存 + 按名取单例"的容器实现。注意它管的是容器内唯一,不是 JVM 内唯一。

8. 常见追问

Q:单例在集群里还是单例吗? 不是。单例的作用域是一个 JVM/进程。集群下每个节点各有一份,若需要全局唯一,得靠分布式锁、数据库唯一约束或选主。

Q:单例有什么危害? 本质是全局状态:难 mock、隐式依赖、并发下共享可变字段容易出问题、初始化失败会拖垮类加载。所以单例对象应当尽量无状态;Spring 的单例 bean 也只保证一份实例,不保证线程安全。

小结

  • 5 种写法:饿汉、懒汉、DCL、静态内部类、枚举;DCL 必须 volatile。
  • 枚举最安全(防反射、防反序列化),静态内部类在"懒加载 + 简洁"上最平衡。
  • 单例是 JVM 级唯一;跨节点唯一要靠分布式协调,且单例对象应尽量无状态。
笔记加载中…