单例模式有几种写法?
2026年01月25日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,主要想考察以下几个层面,而不仅仅是让你罗列名称:
- 对单例模式核心思想与实现细节的理解深度:你是否真正理解 “保证一个类仅有一个实例,并提供一个全局访问点” 这一核心目标。
- 对多线程环境下并发安全的掌握:如何确保在高并发场景下,单例的创建依然是线程安全的,这是考察重点。
- 对 JVM 类加载机制、内存模型(如
volatile)、反射等底层知识的理解:不同实现方式背后的原理是什么?为何有些写法是安全的,有些则存在隐患。 - 实际工程应用与设计权衡能力:你是否了解每种写法的优缺点、适用场景,以及目前公认的最佳实践是什么。这能反映你的工程经验和代码品味。
核心答案
单例模式的主流写法通常可分为 5 种:饿汉式、懒汉式(线程不安全与同步方法版)、双重检查锁(DCL)、静态内部类(Holder) 以及 枚举(Enum)。
从安全性、简洁性、功能性综合来看,枚举(Enum) 是实现单例模式的最佳实践,它由 JVM 从根本上保证线程安全、反射安全和序列化/反序列化安全。静态内部类 则是延迟加载场景下非枚举方式的首选。
深度解析
单例模式看似简单,但 “写对” 并 “写好” 需要考虑诸多细节。下面我们逐一拆解。
1. 饿汉式(Eager Initialization)
- 原理:在类加载的初始化阶段,就通过静态变量创建实例。这利用了 JVM 类加载机制的线程安全性来保证实例唯一。
- 代码示例:
public class EagerSingleton { // 1. 私有静态常量,类加载时即初始化 private static final EagerSingleton INSTANCE = new EagerSingleton(); // 2. 私有构造函数 private EagerSingleton() {} // 3. 公有静态方法,返回唯一实例 public static EagerSingleton getInstance() { return INSTANCE; } } - 优劣分析:
- 优点:实现简单,线程安全(由 JVM 保证)。
- 缺点:非延迟加载。无论是否用到,实例都在启动时创建,若实例创建耗资源或始终未使用,则会造成资源浪费。
- 适用场景:单例实例较小,且程序启动后立即会使用的场景。
2. 懒汉式(Lazy Initialization)
- 基础(线程不安全)版:在
getInstance()中判断并创建,多线程下会创建多个实例,严禁使用。// ❌ 错误示例:线程不安全 public static LazySingleton getInstance() { if (instance == null) { // 多个线程可能同时进入此判断 instance = new LazySingleton(); } return instance; } - 同步方法(线程安全)版:在方法上直接加
synchronized锁。public class SynchronizedSingleton { private static SynchronizedSingleton instance; private SynchronizedSingleton() {} // 使用 synchronized 保证线程安全 public static synchronized SynchronizedSingleton getInstance() { if (instance == null) { instance = new SynchronizedSingleton(); } return instance; } }- 缺点:锁粒度太粗,每次调用
getInstance()都需要同步,性能低下。
- 缺点:锁粒度太粗,每次调用
3. 双重检查锁(Double-Checked Locking, DCL)
- 原理:在懒汉式的基础上,将同步块的范围缩小,并在同步块内外进行两次判空检查。结合
volatile关键字(JDK 5+)防止指令重排序导致的 “部分初始化” 问题。 - 代码示例(标准写法):
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(); // 依赖 volatile 保证写操作先行发生 } } } return instance; } } - 最佳实践与常见误区:
volatile关键字必不可少。没有它,线程可能拿到一个未初始化完全的对象(半初始化状态)。- 这是延迟加载且性能较好的方案,但代码稍显复杂。
4. 静态内部类(Static Inner Class / Holder)
- 原理:利用 JVM 的类加载机制 —— 静态内部类只有在被主动引用时(如调用
getInstance())才会加载,从而实现延迟加载。其静态变量初始化由 JVM 保证线程安全。 - 代码示例:
public class InnerClassSingleton { private InnerClassSingleton() {} // 静态内部类 private static class SingletonHolder { private static final InnerClassSingleton INSTANCE = new InnerClassSingleton(); } public static InnerClassSingleton getInstance() { return SingletonHolder.INSTANCE; // 触发内部类加载和初始化 } } - 优劣分析:
- 优点:实现简洁,线程安全,延迟加载,性能好(无锁)。是非枚举方式中的首选。
- 缺点:无法防止通过反射调用私有构造函数创建新实例(但可以通过在构造器中加判断来防御)。
5. 枚举(Enum)【最佳实践】
- 原理:Java 枚举类型本身的特性保证了其常量(即单例实例)在 JVM 中是唯一的,且其构造器是私有的。JVM 从根本上保证了线程安全、反射安全(JDK 内部禁止使用反射创建枚举实例)和序列化安全(枚举的序列化机制特殊,仅存储名字,反序列化时通过
valueOf获取同名常量)。 - 代码示例:
public enum EnumSingleton { INSTANCE; // 单例实例 // 可以添加任意方法和属性 public void doSomething() { System.out.println("Singleton Business Method."); } } // 使用:EnumSingleton.INSTANCE.doSomething(); - 最佳实践:
- 《Effective Java》作者 Josh Bloch 强烈推荐的方式。
- 代码极度简洁,且能抵御反射和序列化的攻击,是功能最完备的单例实现。
总结
实现单例模式的关键在于确保线程安全、实现延迟加载(按需创建)并防御反射与序列化破坏。在无特殊需求时,优先使用枚举(Enum)实现;若因历史原因不能使用枚举,静态内部类(Holder) 是实现延迟加载单例的优秀选择。