☰
单例模式详解:5 种写法全对比 + 双重检查 volatile 原理 + 枚举最佳实践
2026/10/1 18:41:29 网站建设 项目流程

一、模式定义

单例模式是一种对象创建型设计模式。使用单例模式,可以保证一个类在程序生命周期内只生成一个唯一的实例对象,并提供一个全局访问点来获取该实例。

GOF 对单例模式的正式定义:
保证一个类只有一个实例存在,同时提供访问该实例的全局访问方法。

核心要点有三:

  1. 私有构造器:禁止外部通过new随意创建实例。
  2. 自身持有实例:类内部自行创建并保存唯一实例。
  3. 全局访问方法:对外提供静态方法返回该唯一实例。

单例结构(类图)

Singleton

- Singleton instance

- Singleton()

+ getInstance() : Singleton

关键点:构造器私有,实例由类自己持有,外部只能通过getInstance()获取。


二、应用场景

在应用开发中,以下需求适合使用单例模式:

  • 跨线程共享同一资源或对象:例如 Servlet 环境下的配置中心、连接池、线程池、缓存管理器等。
  • 全局唯一的共享资源:例如日志对象、计数器、ID 生成器,避免多处状态不一致。
  • 性能敏感的大规模系统:通过复用唯一实例,节省频繁new对象带来的创建与 GC 开销。
  • 需要控制资源访问的场景:例如数据库连接、文件句柄等稀缺资源,统一由单例管理。

⚠️ 注意:单例本质上是"全局变量"的受控形态。滥用单例会增加模块耦合、不利于单元测试,应只在确实要求全局唯一时使用。


三、实现方式与对比

下面给出5 种常见实现。其中静态内部类、枚举是工业界最推荐的写法。

实现方式线程安全懒加载防反射防反序列化防克隆推荐度
饿汉式✅❌❌❌❌⭐⭐
懒汉式(同步方法)✅✅❌❌❌⭐⭐
双重检查(DCL)✅✅❌❌❌⭐⭐⭐
静态内部类✅✅❌❌❌⭐⭐⭐⭐
枚举✅✅✅✅✅⭐⭐⭐⭐⭐

说明:除枚举外,其余写法在"反射 / 反序列化 / 克隆"面前都并非绝对安全,详见第四节的防御方案。


1. 饿汉式(Eager)

类加载时就创建实例,线程安全,但即使从未使用也会占用资源。

// 饿汉式publicclassPerson{// 类加载阶段即完成实例化,JVM 保证线程安全publicstaticfinalPersonperson=newPerson();privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}// 构造私有化,禁止外部 newprivatePerson(){}// 全局静态访问方法publicstaticPersongetPerson(){returnperson;}}

点评:实现最简单、绝对线程安全;缺点是不支持懒加载,若该实例很大且一直用不到,会造成资源浪费。


2. 懒汉式(Lazy)

延迟到首次调用时才创建实例。原始版本非线程安全,必须配合synchronized才能用于多线程。

// 懒汉式publicclassPerson{privatestaticPersonperson;privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}privatePerson(){}// ⚠️ 非线程安全:多线程下可能创建出多个实例,禁止在生产使用publicstaticPersongetPerson(){if(person==null){person=newPerson();}returnperson;}// ✅ 线程安全写法:对整个方法加锁,但每次调用都要同步,性能较差publicstaticsynchronizedPersongetPerson1(){if(person==null){person=newPerson();}returnperson;}}

点评:synchronized方法版能保线程安全,但锁粒度太粗,每次获取实例都加锁,高并发下性能瓶颈明显。


3. 双重检查锁(Double-Checked Locking,DCL)

仅对"创建对象"这一步加锁,且只同步一次,兼顾线程安全与性能。关键点:person必须声明为volatile。

// 双重检查publicclassPerson{// 🔴 必须加 volatile:禁止指令重排,保证可见性privatestaticvolatilePersonperson;privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}privatePerson(){}// 全局静态访问方法publicstaticPersongetPerson(){if(person==null){// 第一次检查(无锁,提升性能)synchronized(Person.class){// 只对创建过程加锁if(person==null){// 第二次检查person=newPerson();}}}returnperson;}}

📌 为什么必须加volatile?
person = new Person();在 JVM 中并非原子操作,大致分为:① 分配内存、② 初始化对象、③ 将引用指向内存。由于指令重排,可能先执行 ③ 再执行 ②。此时另一个线程在第一次检查时看到person != null,会拿到一个尚未初始化完成的对象,导致程序出错。volatile通过禁止重排、保证可见性解决此问题(Java 5 之后的volatile语义才真正生效)。


4. 静态内部类(推荐)

利用JVM 类加载机制实现:外部类加载时不会加载内部类,只有首次调用getInstance()时才会加载内部类并创建实例,天然线程安全且懒加载,无需加锁。

// 静态内部类(推荐)publicclassPerson{privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}privatePerson(){}// 静态内部类:外部类加载时不加载它,从而实现懒加载privatestaticclassPersonHolder{privatestaticfinalPersonINSTANCE=newPerson();}// 首次调用时才触发 PersonHolder 加载,JVM 保证线程安全publicstaticPersongetPerson(){returnPersonHolder.INSTANCE;}}

点评:写法简洁、无锁、线程安全、懒加载三者兼具,是不使用枚举时的最佳实践。


5. 枚举(Effective Java 推荐)

《Effective Java》作者 Joshua Bloch 指出:实现单例的最佳方式是使用枚举。它天然防反射攻击、防反序列化破坏,且写法极简。

// 枚举单例(最推荐)publicenumPerson{INSTANCE;privateStringname;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}// 使用:Person.INSTANCE.getName();}

优点:

  • JVM 保证实例唯一且线程安全;
  • 自动处理反序列化(普通单例需重写readResolve()才能防反序列化破坏);
  • 天然抵御反射调用私有构造器(枚举构造器无法通过反射创建);
  • 代码量最少。

缺点:非懒加载(枚举类加载即创建),但通常可接受。


四、单例的"三道坎":反射、反序列化、克隆攻击与防御

很多教程讲完上面的写法就结束了,但手写单例(除枚举外)都能被"攻破"。面试常问,生产也需注意。

1. 反射攻击

即使构造器私有,仍可用setAccessible(true)强行调用私有构造器创建新实例:

Constructor<Person>c=Person.class.getDeclaredConstructor();c.setAccessible(true);Personp2=c.newInstance();// 又多了一个实例!

防御:在私有构造器中加"已实例化则抛异常"的判断(双重检查、静态内部类等适用):

privatePerson(){if(PersonHolder.INSTANCE!=null){// 或 DCL 中的 person != nullthrownewIllegalStateException("单例已存在,禁止反射创建");}}

枚举天然免疫:JVM 在newInstance()中对枚举类型直接抛IllegalArgumentException。

2. 反序列化破坏

若单例类实现了Serializable,反序列化时会新建一个对象,破坏唯一性。

防御:重写readResolve(),返回既有实例:

// 仅补充在可序列化单例类中privateObjectreadResolve(){returnperson;// 或 PersonHolder.INSTANCE / INSTANCE}

枚举无需此处理,序列化/反序列化由 JVM 保证同一实例。

3. 克隆破坏

若单例类实现了Cloneable,调用clone()会复制出另一个对象。

防御:重写clone(),直接返回单例本身(或抛异常):

@OverrideprotectedObjectclone()throwsCloneNotSupportedException{returngetPerson();// 始终返回唯一实例}

小结:只有枚举能一次性免疫这三种破坏;其余写法需手动加防御代码,工程中若无序列化/反射场景,通常可省略,但要知道风险所在。


五、选型建议

  1. 最省心、最安全→ 用枚举(第 5 种),天然防反射、反序列化、克隆。
  2. 需要懒加载、又不想用枚举→ 用静态内部类(第 4 种),无锁且线程安全。
  3. 需兼顾旧 JDK / 特定懒加载语义→ 用双重检查(第 3 种,务必加volatile)。
  4. 实例一定会被用到、追求简单→ 用饿汉式(第 1 种)。
  5. 懒汉式同步方法(第 2 种)性能差,仅作理解用,生产不推荐。

一句话原则:能用枚举就用枚举;不能用枚举就用静态内部类。前两种手写方式主要帮助理解原理,生产环境优先后两者。


六、总结

单例模式看似简单,却藏着volatile指令重排、反射/反序列化/克隆三大破坏等"深水区"。记住结论即可:日常开发首选枚举,其次静态内部类,把双重检查的volatile记牢,基本就能应对 99% 的面试与生产场景。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询