Java 23 种设计模式:从踩坑到精通 | Singleton —— 你写的单例真的安全吗?
Java 23 种设计模式:从踩坑到精通 | Singleton 模式 —— 你写的单例真的安全吗?
摘要:单例模式是创建型模式中最基础却也最容易埋坑的模式。本文从概念出发,拆解单例的12 种实现方式—— 从最简陋的懒汉式到最坚固的枚举,从经典的 DCL 到鲜为人知的 ThreadLocal 与 CAS 实现。同时深入剖析反射、序列化、克隆攻击的破坏手法,并给出防御方案。结合 Spring、JDK 等框架中的单例应用,让你不仅会写,更懂得在何种场景下选择何种实现。读完本文,你将拥有一份可以应对面试与生产环境的单例“兵器谱”。
📖《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 |当前:Singleton 单例模式| 下一篇:工厂模式
🔗 返回系列总目录
1. 从“只有一个实例”说起
为什么我们需要单例?像数据库连接池、配置管理、线程池这类系统资源,应当全局只存在一份;多个实例不仅浪费内存,还会引发状态不一致。单例模式通过私有构造器和静态访问方法,严格控制实例数量。
但“保证只有一个实例”远没有听上去那么简单。接下来,我们将从最基础的实现开始,逐步升级到坚不可摧的版本。
2. 模式定义与 UML
单例模式保证一个类在 JVM 中只有一个实例,并提供一个全局访问点。
- 构造器私有 → 禁止外部 new
- 静态变量持有唯一实例
- 静态
getInstance()作为全局访问口
📌 后续所有实现变体都围绕这一结构进行安全性和性能的增强。
3. 单例模式的 12 种写法
3.1 饿汉式 —— 类加载时直接创建
① 静态常量
publicclassSingleton{publicstaticfinalSingletonINSTANCE=newSingleton();privateSingleton(){}}利用类初始化机制保证线程安全,但不支持延迟加载。
② 静态代码块
publicclassSingleton{privatestaticfinalSingletonINSTANCE;static{INSTANCE=newSingleton();}privateSingleton(){}}适合需要额外初始化逻辑的场景,本质上也是饿汉式。
3.2 懒汉式 —— 用时才创建
③ 线程不安全懒汉
publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}}并发下会创建多个实例,仅适用于单线程环境。
④ 同步方法懒汉
publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}}解决了线程安全问题,但每次访问都持有锁,性能低下。
3.3 双重检查锁定(DCL) —— 性能与安全的平衡
⑤ 标准 DCL
publicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}returninstance;}}volatile确保了对象初始化的可见性和禁止指令重排。
⚠️ 如果去掉
volatile,可能拿到半初始化的对象,JIT 重排序坑了无数人。
3.4 静态内部类 —— 优雅的懒加载
⑥ 静态内部类
publicclassSingleton{privateSingleton(){}privatestaticclassHolder{privatestaticfinalSingletonINSTANCE=newSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}}JVM 保证了类加载的线程安全,同时做到了懒加载,非常推荐。
3.5 枚举 —— 攻不破的堡垒
⑦ 枚举单例
publicenumSingleton{INSTANCE;publicvoiddoSomething(){}}- 反射攻击无效:
Constructor.newInstance()对枚举抛出异常 - 序列化安全:反序列化自动返回同一实例
- 克隆安全:枚举不可克隆
3.6 容器式单例 —— 管理多种单例
⑧ 登记式(容器式)单例
publicclassSingletonManager{privatestaticMap<String,Object>map=newConcurrentHashMap<>();privateSingletonManager(){}publicstaticvoidregisterService(Stringkey,Objectinstance){map.putIfAbsent(key,instance);}publicstaticObjectgetService(Stringkey){returnmap.get(key);}}适合系统中有多个需要管理的单例对象,Spring 的容器本质上就是这种思想的体现。
3.7 线程内单例 —— 每个线程一个实例
⑨ ThreadLocal 单例
publicclassSingleton{privatestaticfinalThreadLocal<Singleton>threadLocal=ThreadLocal.withInitial(Singleton::new);privateSingleton(){}publicstaticSingletongetInstance(){returnthreadLocal.get();}}保证每个线程内只有一个实例,多线程间不共享。常用于数据库连接或事务上下文。
3.8 无锁实现 —— CAS 乐观锁
⑩ CAS 原子操作
publicclassSingleton{privatestaticfinalAtomicReference<Singleton>INSTANCE=newAtomicReference<>();privateSingleton(){}publicstaticSingletongetInstance(){for(;;){Singletoncurrent=INSTANCE.get();if(current!=null)returncurrent;current=newSingleton();if(INSTANCE.compareAndSet(null,current))returncurrent;}}}通过AtomicReference实现无锁线程安全,适合高并发场景。但要注意如果构造函数很重,可能会多次执行new。
3.9 防止破坏的加强版(防御式写法)
⑪ 静态内部类 + 序列化/反射防御
在静态内部类的基础上,增加:
publicclassSingletonimplementsSerializable{privatestaticfinallongserialVersionUID=1L;privateSingleton(){if(Holder.INSTANCE!=null){thrownewRuntimeException("禁止反射创建实例");}}privatestaticclassHolder{privatestaticfinalSingletonINSTANCE=newSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}// 防止反序列化破坏privateObjectreadResolve(){returnHolder.INSTANCE;}}⑫ 枚举的变体:带属性的枚举
publicenumConfig{INSTANCE;privatePropertiesprops=newProperties();Config(){/* 加载配置 */}publicStringgetProperty(Stringkey){returnprops.getProperty(key);}}枚举不仅可以做单例,还能封装复杂的业务逻辑。
4. 单例的破坏与防御(进阶)
除了反射和反序列化,还有克隆攻击:
publicclassSingletonimplementsCloneable{privatestaticfinalSingletonINSTANCE=newSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returnINSTANCE;}@OverrideprotectedObjectclone()throwsCloneNotSupportedException{thrownewCloneNotSupportedException("单例禁止克隆");// 或者直接 return INSTANCE;}}防御总结表:
| 攻击方式 | 防御手段 |
|---|---|
| 反射 | 构造器内判断实例是否已存在,抛异常 |
| 序列化 | 添加readResolve()方法返回已存在实例 |
| 克隆 | 重写clone()方法,抛异常或返回自身 |
枚举天生免疫这三种攻击。
5. 12 种写法对比一览(含推荐指数)
| 写法 | 线程安全 | 懒加载 | 防反射 | 防序列化 | 推荐指数 |
|---|---|---|---|---|---|
| 饿汉-常量 | ✓ | ✗ | ✗ | ✗ | ★★☆☆ |
| 饿汉-静态块 | ✓ | ✗ | ✗ | ✗ | ★★☆☆ |
| 懒汉-不安全 | ✗ | ✓ | ✗ | ✗ | ★☆☆☆ |
| 懒汉-同步 | ✓ | ✓ | ✗ | ✗ | ★★☆☆ |
| DCL | ✓ | ✓ | ✗ | ✗(可防) | ★★★★ |
| 静态内部类 | ✓ | ✓ | ✗(可防) | ✗(可防) | ★★★★★ |
| 枚举 | ✓ | ✓ | ✓ | ✓ | ★★★★★ |
| 容器式 | ✓ | ✓ | ✗ | ✗ | ★★★☆ |
| ThreadLocal | 线程内 | ✓ | ✗ | ✗ | ★★☆☆ |
| CAS | ✓ | ✓ | ✗ | ✗ | ★★★☆ |
| 静态内部类+防御 | ✓ | ✓ | ✓ | ✓ | ★★★★★ |
| 带属性枚举 | ✓ | ✓ | ✓ | ✓ | ★★★★★ |
✅建议:若无需继承其他类,优先使用枚举;否则选用静态内部类并添加防御代码。
6. 常见误区与面试高频题
❌ 误区1:单例就是全局变量
单例是控制实例化,可包含业务逻辑,不是简单的static变量。
❌ 误区2:加了 synchronized 就绝对线程安全
DCL 的指令重排问题已证明光有同步块不够,还需volatile。
❌ 误区3:枚举单例无法懒加载
枚举类在首次被使用时才会初始化,效果类似饿汉式,但同样是“用时加载”。
💡 面试高频追问
- 静态内部类为什么是懒加载的?→ 内部类在首次被引用时才会被加载。
- 枚举单例能否被反射破坏?→ 不能,
Constructor.newInstance()中会对枚举进行特殊判断并抛出异常。 - ThreadLocal 单例的典型应用场景?→ 每个线程需要独立的数据库连接或事务上下文时。
- CAS 实现单例的缺点?→ 构造函数可能被执行多次,资源消耗大时不推荐。
7. 框架中的单例应用
- Spring:默认 Bean 作用域为
singleton,DefaultSingletonBeanRegistry用Map缓存所有单例 Bean,本质是容器式单例。 - JDK:
java.lang.Runtime采用饿汉式;java.lang.System中的许多工具方法也依赖单例思想。 - Log4j/Logback:
LoggerFactory.getLogger()返回的 Logger 通常是静态内部类或容器式单例。
8. 你应该选哪一种?
- 如果单例不需要继承其他类,优先使用枚举。
- 如果需要继承或需要懒加载,使用静态内部类 + 防御代码。
- 在高并发且构造函数较轻时,CAS 实现是一个有趣的替代。
- 若要在每个线程内保持唯一实例(如用户会话上下文),选择ThreadLocal 单例。
- 当系统中有许多单例需要统一管理时,参考容器式单例。
🧭 《Java 23 种设计模式:从踩坑到精通》快速导航
- 开篇:系列介绍与目录
- 上一篇:系列介绍与目录
- 当前:Singleton 单例模式(你在这里)
- 下一篇:工厂模式三兄弟
- 创建型模式汇总:单例、工厂、建造者、原型
- 结构型模式汇总:适配器、装饰器、代理……
- 行为型模式汇总:观察者、策略、模板方法……
🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 24 篇文章彻底吃透设计模式,让代码设计成为你的本能。
