Java 23 种设计模式:从踩坑到精通 | 单例模式 —— 你写的真的安全吗?

摘要:你写的单例真的线程安全吗?本文一口气拆解 12 种 实现方式——从饿汉式到枚举,从 DCL 到 CAS,附反射/序列化/克隆攻击防御测试。结合 Spring、JDK 及 AI 时代新场景,帮你彻底掌握单例的正确选型与安全防御。读完本文,你将拥有一份可以应对面试与生产环境的单例“兵器谱”。

📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 当前:单例模式 | 下一篇:工厂模式
🔗 返回系列总目录


1. 从“只有一个实例”说起

为什么我们需要单例?像数据库连接池、配置管理、线程池这类系统资源,应当全局只存在一份;多个实例不仅浪费内存,还会引发状态不一致。单例模式通过私有构造器和静态访问方法,严格控制实例数量。

但“保证只有一个实例”远没有听上去那么简单。接下来,我们将从最基础的实现开始,逐步升级到坚不可摧的版本。

1.1 单例模式的作用与学习意义

单例模式的核心作用是 确保一个类在整个系统运行期间有且仅有一个实例,并提供一个全局访问点。它能有效避免因创建多个实例导致的资源浪费、状态不一致或行为冲突。

学习单例的意义远不止“背出一种写法”:

  • 面试硬通货:它是面试中最高频的设计模式之一,关于线程安全(DCL + volatile)、反射/序列化破坏与防御、枚举单例等追问,直接区分背题与真懂。
  • 读懂框架的钥匙:Spring 容器默认的单例 Bean、JDK 的 Runtime 类、日志工厂等,本质都是单例思想的体现。理解了它,你才能真正看懂这些设计的意图。
  • 多线程与安全编程的实战入口:从饿汉式到 DCL 再到枚举,单例模式的演进浓缩了 JMM、指令重排、CAS、序列化机制等核心知识点,是学习并发与底层原理的绝佳抓手。
  • 设计原则的第一课:单例虽小,却蕴含了单一职责、开闭原则、依赖倒置等 SOLID 思想,是通往系统设计能力的启蒙模式。

2. 模式定义与 UML

单例模式保证一个类在 JVM 中只有一个实例,并提供一个全局访问点。
单例模式

  • 构造器私有 → 禁止外部 new
  • 静态变量持有唯一实例
  • 静态 getInstance() 作为全局访问口

📌 后续所有实现变体都围绕这一结构进行安全性和性能的增强。

3. 单例模式的 12 种写法

3.1 饿汉式 —— 类加载时直接创建

① 静态常量

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
}

✅ 最简单、天生线程安全;❌ 无懒加载,可能浪费内存。

② 静态代码块

public class Singleton {
    private static final Singleton INSTANCE;
    static {
        INSTANCE = new Singleton();
    }
    private Singleton() {}
}

✅ 可处理初始化异常;❌ 本质上仍是饿汉,同样无懒加载。

3.2 懒汉式 —— 用时才创建

③ 线程不安全懒汉

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

⚠️ 并发下会创建多个实例,仅单线程环境可用,生产禁用

④ 同步方法懒汉

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

✅ 线程安全;❌ 每次调用都加锁,高并发下性能极差。

3.3 双重检查锁定(DCL) —— 性能与安全的平衡

⑤ 标准 DCL

public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

✅ 高性能 + 懒加载;⚠️ volatile 缺一不可,否则指令重排可能返回半初始化对象。

3.4 静态内部类 —— 优雅的懒加载

⑥ 静态内部类

public class Singleton {
    private Singleton() {}
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

✅ 懒加载 + 无锁 + 线程安全,日常开发首选之一,非常推荐。

3.5 枚举 —— 攻不破的堡垒

⑦ 枚举单例

public enum Singleton {
    INSTANCE;
    public void doSomething() {}
}
  • 反射攻击无效:Constructor.newInstance() 对枚举抛出异常
  • 序列化安全:反序列化自动返回同一实例
  • 克隆安全:枚举不可克隆

✅ 防反射、防序列化、防克隆,代码最少,最安全的单例写法。

3.6 容器式单例 —— 管理多种单例

⑧ 登记式(容器式)单例

public class SingletonManager {
    private static Map<String, Object> map = new ConcurrentHashMap<>();
    private SingletonManager() {}
    public static void registerService(String key, Object instance) {
        map.putIfAbsent(key, instance);
    }
    public static Object getService(String key) {
        return map.get(key);
    }
}

✅ 适合管理多种单例对象;❌ 需注意线程安全(使用 ConcurrentHashMap)。

3.7 线程内单例 —— 每个线程一个实例

⑨ ThreadLocal 单例

public class Singleton {
    private static final ThreadLocal<Singleton> threadLocal = ThreadLocal.withInitial(Singleton::new);
    private Singleton() {}
    public static Singleton getInstance() {
        return threadLocal.get();
    }
}

✅ 每个线程一个实例,适合线程内资源隔离(如数据库连接)。

3.8 无锁实现 —— CAS 乐观锁

⑩ CAS 原子操作

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
    private Singleton() {}
    public static Singleton getInstance() {
        for (;;) {
            Singleton current = INSTANCE.get();
            if (current != null) return current;
            current = new Singleton();
            if (INSTANCE.compareAndSet(null, current)) return current;
        }
    }
}

✅ 无锁高并发;⚠️ 构造函数可能被执行多次,轻量对象适用。
通过 AtomicReference 实现无锁线程安全,适合高并发场景。但要注意如果构造函数很重,可能会多次执行 new

3.9 防止破坏的加强版(防御式写法)

⑪ 静态内部类 + 序列化/反射防御

在静态内部类的基础上,增加:

public class Singleton implements Serializable {
    private static final long serialVersionUID = 1L;
    private Singleton() {
        if (Holder.INSTANCE != null) {
            throw new RuntimeException("禁止反射创建实例");
        }
    }
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
    // 防止反序列化破坏
    private Object readResolve() {
        return Holder.INSTANCE;
    }
}

✅ 静态内部类的增强版,生产环境推荐,完整抵御反射、序列化攻击。

⑫ 枚举的变体:带属性的枚举

public enum Config {
    INSTANCE;
    private Properties props = new Properties();
    Config() { /* 加载配置 */ }
    public String getProperty(String key) { return props.getProperty(key); }
}

✅ 枚举的安全 + 复杂业务封装能力,单例配置管理首选。

4. 单例的破坏与防御

除了反射和反序列化,还有克隆攻击:

public class Singleton implements Cloneable {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() { return INSTANCE; }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("单例禁止克隆");
        // 或者直接 return INSTANCE;
    }
}

防御总结表

攻击方式 防御手段
反射 构造器内判断实例是否已存在,抛异常
序列化 添加 readResolve() 方法返回已存在实例
克隆 重写 clone() 方法,抛异常或返回自身

枚举天生免疫这三种攻击。

5. 12 种写法对比一览

写法 线程安全 懒加载 防反射 防序列化 推荐指数
饿汉-常量 ★★☆☆
饿汉-静态块 ★★☆☆
懒汉-不安全 ★☆☆☆
懒汉-同步 ★★☆☆
DCL ✗(可防) ★★★★
静态内部类 ✗(可防) ✗(可防) ★★★★★
枚举 ★★★★★
容器式 ★★★☆
ThreadLocal 线程内 ★★☆☆
CAS ★★★☆
静态内部类+防御 ★★★★★
带属性枚举 ★★★★★

建议:若无需继承其他类,优先使用枚举;否则选用静态内部类并添加防御代码。


📌 正在追更《Java 23 种设计模式:从踩坑到精通》?
点击右上角「关注」,不错过下一篇——工厂模式:还在写一坨 if-else 创建对象吗?
简单工厂、工厂方法、抽象工厂,你真的分得清吗?


6. 常见误区与面试高频题

❌ 误区1:单例就是全局变量
单例是控制实例化,可包含业务逻辑,不是简单的 static 变量。

❌ 误区2:加了 synchronized 就绝对线程安全
DCL 的指令重排问题已证明光有同步块不够,还需 volatile

❌ 误区3:枚举单例无法懒加载
枚举类在首次被使用时才会初始化,效果类似饿汉式,但同样是“用时加载”。

💡 面试高频追问

  • 静态内部类为什么是懒加载的?→ 内部类在首次被引用时才会被加载。
  • 枚举单例能否被反射破坏?→ 不能,Constructor.newInstance() 中会对枚举进行特殊判断并抛出异常。
  • ThreadLocal 单例的典型应用场景?→ 每个线程需要独立的数据库连接或事务上下文时。
  • CAS 实现单例的缺点?→ 构造函数可能被执行多次,资源消耗大时不推荐。

7. 框架与 AI 时代中的单例应用

7.1 经典框架中的单例

  • Spring:默认 Bean 作用域为 singletonDefaultSingletonBeanRegistryMap 缓存所有单例 Bean,本质是容器式单例。
  • JDKjava.lang.Runtime 采用饿汉式;java.lang.System 中的许多工具方法也依赖单例思想。
  • Log4j/LogbackLoggerFactory.getLogger() 返回的 Logger 通常是静态内部类或容器式单例。

7.2 2026 AI 时代的新场景

在 AI 时代,单例模式非但没有过时,反而在管理共享资源、维护全局状态等新场景中演变为“能力中枢”:

  • 模型服务与推理客户端:大语言模型(LLM)的客户端对象和向量数据库连接是重量级资源,必须作为单例复用以避免重复加载。现代 AI 框架通过单例统一管理这些 API 客户端,保证效率与状态一致性。
  • AI 网关与模型路由器:当系统需要调用多个 LLM(如 GPT-4o、Claude、DeepSeek)时,模型路由器作为单例集中管理路由策略、成本控制、熔断降级等复杂逻辑,确保决策一致性。
  • 智能体(Agent)上下文管理:在多智能体协同场景中,单例为每个 Agent 提供独立的上下文空间,保证不同 Agent 间状态隔离,避免信息串扰或污染。
  • Prompt 模板与安全管理:提示词模板和 API Key 管理器通常以单例形式运行,实现统一加载、版本管理和安全轮转。
  • 遥测与可观测性中枢:全局的 Metrics 收集器、Trace 管理器、日志聚合器通常也是单例,保证数据一致性并避免重复初始化开销。

2026 年,单例模式依然是控制全局资源和状态的关键工具。它的角色已经演变为 AI 应用的“核心大管家”或“能力枢纽”,负责从模型加载、智能体状态、提示词到遥测数据的全方位管理与调度。


8. 你应该选哪一种?

  • 如果单例不需要继承其他类,优先使用枚举
  • 如果需要继承或需要懒加载,使用 静态内部类 + 防御代码
  • 在高并发且构造函数较轻时,CAS 实现 是一个有趣的替代。
  • 若要在每个线程内保持唯一实例(如用户会话上下文),选择 ThreadLocal 单例
  • 当系统中有许多单例需要统一管理时,参考 容器式单例

附录:Singleton UML 源码

@startuml
skinparam backgroundColor #FEFEFE
class Singleton {
  - {static} uniqueInstance : Singleton
  - Singleton()
  + {static} getInstance() : Singleton
}
@enduml

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
🚀 下一篇工厂模式 —— 还在写一坨 if-else?简单工厂→工厂方法→抽象工厂全演进
📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐