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{publicstaticfinalSingletonINSTANCEnewSingleton();privateSingleton(){}}利用类初始化机制保证线程安全但不支持延迟加载。② 静态代码块publicclassSingleton{privatestaticfinalSingletonINSTANCE;static{INSTANCEnewSingleton();}privateSingleton(){}}适合需要额外初始化逻辑的场景本质上也是饿汉式。3.2 懒汉式 —— 用时才创建③ 线程不安全懒汉publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){instancenewSingleton();}returninstance;}}并发下会创建多个实例仅适用于单线程环境。④ 同步方法懒汉publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instancenull){instancenewSingleton();}returninstance;}}解决了线程安全问题但每次访问都持有锁性能低下。3.3 双重检查锁定DCL —— 性能与安全的平衡⑤ 标准 DCLpublicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}}returninstance;}}volatile确保了对象初始化的可见性和禁止指令重排。⚠️ 如果去掉volatile可能拿到半初始化的对象JIT 重排序坑了无数人。3.4 静态内部类 —— 优雅的懒加载⑥ 静态内部类publicclassSingleton{privateSingleton(){}privatestaticclassHolder{privatestaticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}}JVM 保证了类加载的线程安全同时做到了懒加载非常推荐。3.5 枚举 —— 攻不破的堡垒⑦ 枚举单例publicenumSingleton{INSTANCE;publicvoiddoSomething(){}}反射攻击无效Constructor.newInstance()对枚举抛出异常序列化安全反序列化自动返回同一实例克隆安全枚举不可克隆3.6 容器式单例 —— 管理多种单例⑧ 登记式容器式单例publicclassSingletonManager{privatestaticMapString,ObjectmapnewConcurrentHashMap();privateSingletonManager(){}publicstaticvoidregisterService(Stringkey,Objectinstance){map.putIfAbsent(key,instance);}publicstaticObjectgetService(Stringkey){returnmap.get(key);}}适合系统中有多个需要管理的单例对象Spring 的容器本质上就是这种思想的体现。3.7 线程内单例 —— 每个线程一个实例⑨ ThreadLocal 单例publicclassSingleton{privatestaticfinalThreadLocalSingletonthreadLocalThreadLocal.withInitial(Singleton::new);privateSingleton(){}publicstaticSingletongetInstance(){returnthreadLocal.get();}}保证每个线程内只有一个实例多线程间不共享。常用于数据库连接或事务上下文。3.8 无锁实现 —— CAS 乐观锁⑩ CAS 原子操作publicclassSingleton{privatestaticfinalAtomicReferenceSingletonINSTANCEnewAtomicReference();privateSingleton(){}publicstaticSingletongetInstance(){for(;;){SingletoncurrentINSTANCE.get();if(current!null)returncurrent;currentnewSingleton();if(INSTANCE.compareAndSet(null,current))returncurrent;}}}通过AtomicReference实现无锁线程安全适合高并发场景。但要注意如果构造函数很重可能会多次执行new。3.9 防止破坏的加强版防御式写法⑪ 静态内部类 序列化/反射防御在静态内部类的基础上增加publicclassSingletonimplementsSerializable{privatestaticfinallongserialVersionUID1L;privateSingleton(){if(Holder.INSTANCE!null){thrownewRuntimeException(禁止反射创建实例);}}privatestaticclassHolder{privatestaticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}// 防止反序列化破坏privateObjectreadResolve(){returnHolder.INSTANCE;}}⑫ 枚举的变体带属性的枚举publicenumConfig{INSTANCE;privatePropertiespropsnewProperties();Config(){/* 加载配置 */}publicStringgetProperty(Stringkey){returnprops.getProperty(key);}}枚举不仅可以做单例还能封装复杂的业务逻辑。4. 单例的破坏与防御进阶除了反射和反序列化还有克隆攻击publicclassSingletonimplementsCloneable{privatestaticfinalSingletonINSTANCEnewSingleton();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 作用域为singletonDefaultSingletonBeanRegistry用Map缓存所有单例 Bean本质是容器式单例。JDKjava.lang.Runtime采用饿汉式java.lang.System中的许多工具方法也依赖单例思想。Log4j/LogbackLoggerFactory.getLogger()返回的 Logger 通常是静态内部类或容器式单例。8. 你应该选哪一种如果单例不需要继承其他类优先使用枚举。如果需要继承或需要懒加载使用静态内部类 防御代码。在高并发且构造函数较轻时CAS 实现是一个有趣的替代。若要在每个线程内保持唯一实例如用户会话上下文选择ThreadLocal 单例。当系统中有许多单例需要统一管理时参考容器式单例。 《Java 23 种设计模式从踩坑到精通》快速导航开篇系列介绍与目录上一篇系列介绍与目录当前Singleton 单例模式你在这里下一篇工厂模式三兄弟创建型模式汇总单例、工厂、建造者、原型结构型模式汇总适配器、装饰器、代理……行为型模式汇总观察者、策略、模板方法…… 关注《Java 23 种设计模式从踩坑到精通》用 24 篇文章彻底吃透设计模式让代码设计成为你的本能。