34_Java设计模式之单例模式

发布时间:2026/6/13 9:54:03

34_Java设计模式之单例模式 Java设计模式之单例模式文章目录Java设计模式之单例模式前言一、饿汉式Eager Initialization二、懒汉式Lazy Initialization三、双重检查锁定DCL - Double-Checked Locking四、静态内部类推荐五、枚举单例最安全六、序列化安全问题总结✅ 亮点总结适用场景扩展方向前言**单例模式Singleton Pattern**是创建型设计模式中最基础也最常用的模式之一。它确保一个类在整个JVM中只有一个实例并提供一个全局访问点。典型场景包括配置管理器、数据库连接池、Spring容器中的Bean等。本文将从饿汉式到枚举式逐一分析每种实现方式的原理与适用场景。面试视角单例模式几乎是所有Java面试的必考设计模式面试官常从最基础的写一个单例开始然后逐步追问线程安全、双重检查锁定的volatile作用、序列化破坏单例、反射破坏单例、枚举单例的原理等。本文覆盖了这些层层递进的考点建议你不仅看代码更要理解每种实现方式的设计思路和适用边界。一、饿汉式Eager Initialization类加载时就创建实例简单可靠天生线程安全publicclassEagerSingleton{// 类加载时立即实例化privatestaticfinalEagerSingletonINSTANCEnewEagerSingleton();// 私有构造器防止外部newprivateEagerSingleton(){// 防止反射破坏if(INSTANCE!null){thrownewRuntimeException(单例已存在不允许反射创建);}}publicstaticEagerSingletongetInstance(){returnINSTANCE;}}优点实现简单线程安全。缺点无论是否使用都会创建实例可能浪费内存。如果实例化依赖某些运行时参数则无法使用。适用于对象轻量且一定会被使用的场景。实际项目中的考量饿汉式的浪费内存在大多数场景下被夸大了——一个配置管理器或连接池对象通常只占几KB在几个GB的JVM堆内存中微乎其微。所以如果你不需要延迟加载饿汉式是完全可接受的选择。Spring容器中的单例Bean本质上就是饿汉式默认在启动时完成实例化这也是Spring团队认为启动时就发现问题比运行时才发现问题更重要的设计哲学。二、懒汉式Lazy Initialization延迟加载首次调用时才创建// 线程不安全版本publicclassLazySingleton{privatestaticLazySingletoninstance;privateLazySingleton(){}publicstaticLazySingletongetInstance(){if(instancenull){instancenewLazySingleton();}returninstance;}}上述代码在多线程环境下可能创建多个实例。修复方法——加synchronized// 同步方法版本publicclassSyncLazySingleton{privatestaticSyncLazySingletoninstance;privateSyncLazySingleton(){}publicstaticsynchronizedSyncLazySingletongetInstance(){if(instancenull){instancenewSyncLazySingleton();}returninstance;}}虽然线程安全了但每次调用getInstance()都要加锁性能较差。三、双重检查锁定DCL - Double-Checked Locking在保证线程安全的同时尽量减少同步开销publicclassDCLSingleton{// volatile 防止指令重排序privatestaticvolatileDCLSingletoninstance;privateDCLSingleton(){if(instance!null){thrownewRuntimeException(禁止反射破坏单例);}}publicstaticDCLSingletongetInstance(){if(instancenull){// 第一重检查synchronized(DCLSingleton.class){if(instancenull){// 第二重检查instancenewDCLSingleton();// 非原子操作}}}returninstance;}}volatile的作用instance new DCLSingleton()不是原子操作分为三步①分配内存 → ②初始化对象 → ③将引用指向内存。JIT可能将步骤②和③重排序。线程A执行到③但未完成②时线程B在第一重检查发现instance ! null拿到一个未初始化完成的对象。volatile禁止了这种指令重排序。这里需要强调一个细节volatile对指令重排序的禁止能力是在Java 5之后才修复的。如果你还在维护Java 1.4甚至更早的代码DCL是不可靠的尽管这种情况现在极为罕见。这也是为什么有些老项目中会看到各种奇技淫巧的单例实现——其实只是那个年代JMM不完善的产物。四、静态内部类推荐利用类加载机制保证线程安全同时实现延迟加载publicclassHolderSingleton{privateHolderSingleton(){}// 静态内部类只有在被调用时才会加载privatestaticclassHolder{privatestaticfinalHolderSingletonINSTANCEnewHolderSingleton();}publicstaticHolderSingletongetInstance(){returnHolder.INSTANCE;// 触发Holder类加载}}JVM保证类的加载是线程安全的Holder类只在getInstance()首次调用时加载并初始化INSTANCE。这种方式代码简洁、线程安全、延迟加载是最推荐的实现方式之一。尽管静态内部类很优雅但它有一个微小缺点无法在构造时传入参数。如果单例的创建依赖外部配置如从配置文件读取连接信息静态内部类的方式就不太方便了——这时DCL或枚举配合初始化方法会更灵活。在Spring中可以通过Value注入配置后在PostConstruct方法中初始化单例所需的资源。五、枚举单例最安全利用枚举类型天然单例、防反射、防序列化的特性publicenumEnumSingleton{INSTANCE;// 可以在枚举中添加方法和字段privateStringconfig;publicvoidsetConfig(Stringconfig){this.configconfig;}publicStringgetConfig(){returnconfig;}publicvoiddoSomething(){System.out.println(执行单例方法: config);}}// 使用EnumSingleton.INSTANCE.setConfig(数据库连接串);EnumSingleton.INSTANCE.doSomething();枚举单例的优势JVM层面保证只有一个实例天然防止反射攻击Constructor.newInstance()对枚举抛出异常天然防止序列化破坏反序列化返回同一个实例代码极简《Effective Java》作者Joshua Bloch也推荐这种方式。唯一的缺点是无法延迟加载枚举类加载时就初始化但这在大多数场景下不是问题。深入理解枚举单例为什么能防反射因为在Constructor.newInstance()源码中显式判断了如果类是Enum类型直接抛出IllegalArgumentException: Cannot reflectively create enum objects。为什么能防序列化因为Java序列化对枚举有特殊处理——反序列化时不是创建新对象而是通过Enum.valueOf()方法按名称查找已存在的枚举实例。这两点不是巧合而是JVM规范特意为枚举设计的保障。六、序列化安全问题普通单例类在反序列化时会创建新对象破坏单例。解决方式——添加readResolve()方法importjava.io.Serializable;publicclassSafeSerializableSingletonimplementsSerializable{privatestaticfinalSafeSerializableSingletonINSTANCEnewSafeSerializableSingleton();privateSafeSerializableSingleton(){}publicstaticSafeSerializableSingletongetInstance(){returnINSTANCE;}// 反序列化时返回已有实例而非创建新对象privateObjectreadResolve(){returnINSTANCE;}}总结实现方式线程安全延迟加载防反射防序列化推荐度饿汉式是否可防需处理★★★同步懒汉式是是需处理需处理★★DCL是是需处理需处理★★★★静态内部类是是需处理需处理★★★★★枚举是否天然天然★★★★★实际开发中推荐优先使用枚举单例或静态内部类方式。Spring框架中单例Bean的实现本质上类似于饿汉式在容器启动时就完成实例化。最后的忠告单例模式虽好但不要滥用。它本质上是全局状态会带来隐式的耦合使单元测试变得困难需要使用反射或Mockito的InjectMocks等黑魔法来替换单例。如果你的单例越来越多可能意味着设计上需要重新审视——是否可以用依赖注入来代替全局访问。在Spring项目中单例Bean是由容器管理的你不需要手写单例模式只需将类标注为Component并确保它是无状态的即可。✅ 亮点总结五种实现方式饿汉式、同步懒汉式、DCL、静态内部类、枚举的线程安全、延迟加载、防反射特性全对比DCL的双重检查 volatile禁止指令重排Java 5内存模型的经典面试考点静态内部类利用JVM类加载机制保证线程安全且延迟加载实现简洁优雅枚举单例天然防反射和防序列化——Constructor.newInstance()抛异常readResolve()无需手写序列化破坏单例的问题与readResolve()解决方案Jackson反序列化同理适用场景Spring Bean的生命周期管理——理解容器启动时如何创建和管理单例Bean数据库连接池——整个应用共享一个连接池实例避免反复创建销毁配置管理器——加载一次配置文件全局共享配置对象扩展方向Spring单例 vs 设计模式单例了解Spring IoC容器的单例Bean作用域与单例模式的差异推荐阅读下一篇Java设计模式之工厂模式CAS实现无锁单例用AtomicReference CAS 实现高性能非阻塞单例分布式环境下的单例基于Redis分布式锁或Zookeeper选主实现跨JVM的单例

相关新闻