走进 Gang of Four 设计模式:单例模式

📅2026/7/13 13:48:51 👁️次浏览
走进 Gang of Four 设计模式:单例模式
走进 Gang of Four 设计模式单例模式说明不仅告诉你怎么用更告诉你为什么这样设计前置知识Java 面向对象基础构造方法、static、synchronized、多线程基础 设计模式分类总览GoF 23 种设计模式可按两个维度交叉分类类/对象处理方式 ×创建型/结构型/行为型目的。维度创建型Creational结构型Structural行为型Behavioral类Class通过继承复用Factory Method工厂方法Adapter类适配器Interpreter解释器Template Method模板方法对象Object通过组合/聚合复用Abstract Factory抽象工厂Builder建造者Prototype原型Singleton单例Adapter对象适配器Bridge桥接Composite组合Decorator装饰Facade外观Flyweight享元Proxy代理Chain of Resp.责任链Command命令Iterator迭代器Mediator中介者Memento备忘录Observer观察者State状态Strategy策略Visitor访问者本文档聚焦创建型-对象模式中的Singleton单例其余创建型对象模式包括 Abstract Factory、Builder、Prototype。 目录模式概述六种实现方式六种实现对比总表Spring 单例分析单例模式生态应用总结1. 模式概述1.1 要解决什么问题核心矛盾某些对象在系统中只需要一个实例如配置管理器、连接池、日志工厂但如果每次使用时都new一个会导致资源浪费、状态不一致甚至系统错误。// ❌ 反面教材每次 new 一个资源浪费且状态无法统一ConfigManagercfg1newConfigManager();ConfigManagercfg2newConfigManager();cfg1.set(theme,dark);System.out.println(cfg2.get(theme));// null —— 状态不同步解决方案确保一个类只有一个实例并提供全局访问点。1.2 模式定义单例模式确保一个类只有一个实例并提供一个全局访问点来访问该实例。这是 GoF 《设计模式》中定义的创建型模式也是 Java 开发中最常用、也最容易用错的模式之一。1.3 三个核心特征特征说明私有构造方法防止外部通过new创建实例自我实例化类自己负责创建唯一实例静态成员变量全局访问点提供静态方法getInstance()返回该实例2. 六种实现方式2.1 懒汉式线程不安全publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){instancenewSingleton();}returninstance;}}维度评价Lazy 初始化✅ 是线程安全❌ 否多线程下可能创建多个实例实现难度⭐ 易风险线程 A 执行到if (instance null)后切换线程 B 也执行到同一位置两个线程会各自创建实例破坏了单例。2.2 懒汉式线程安全publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instancenull){instancenewSingleton();}returninstance;}}维度评价Lazy 初始化✅ 是线程安全✅ 是但代价大实现难度⭐ 易问题synchronized加在整个方法上每次调用getInstance()都需要获取锁即使实例已经创建。并发高时性能损失严重。2.3 饿汉式publicclassSingleton{privatestaticfinalSingletoninstancenewSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returninstance;}}维度评价Lazy 初始化❌ 否类加载时即创建线程安全✅ 是类加载线程安全由 JVM 保证实现难度⭐ 易优点实现最简单无锁、线程安全。代价类加载时就创建实例如果该实例从未被使用会造成内存浪费。2.4 双检锁DCL, Double-Checked LockingpublicclassSingleton{privatestaticvolatileSingletonsingleton;privateSingleton(){}publicstaticSingletongetSingleton(){if(singletonnull){// 第一次检查synchronized(Singleton.class){// 加锁if(singletonnull){// 第二次检查singletonnewSingleton();}}}returnsingleton;}}维度评价Lazy 初始化✅ 是线程安全✅ 是实现难度⭐⭐⭐ 较复杂关键细节要素作用第一次检查if (singleton null)避免实例已创建后不必要的加锁提升性能synchronized保证同一时间只有一个线程进入创建代码块第二次检查if (singleton null)防止多个线程同时通过第一次检查后重复创建volatile禁止指令重排序确保new Singleton()的写操作对其他线程立即可见为什么需要volatilesingleton new Singleton()在 JVM 中分为三步① 分配内存 ② 初始化对象 ③ 将引用指向内存。如果发生指令重排序变成 ①→③→②线程 B 在步骤③后看到引用不为 null直接返回了未初始化完成的对象导致使用时报错。volatile禁止了这种重排序。2.5 静态内部类Holder 模式publicclassSingleton{privateSingleton(){}privatestaticclassSingletonHolder{privatestaticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnSingletonHolder.INSTANCE;}}维度评价Lazy 初始化✅ 是内部类在被调用时才加载线程安全✅ 是类加载机制保证实现难度⭐⭐ 中原理JVM 在类加载阶段会执行clinit()方法该方法由 JVM 自身保证同步。SingletonHolder类只有在getInstance()方法中首次被引用时才会被 JVM 加载此时才创建INSTANCE实现了延迟加载 线程安全 零同步开销的完美组合。这是兼顾性能与简洁性的最佳实践在不需要处理序列化/反射攻击的常规场景下优先推荐。2.6 枚举publicenumSingleton{INSTANCE;publicvoidwhateverMethod(){// 业务方法}}维度评价Lazy 初始化❌ 否线程安全✅ 是实现难度⭐ 易优势《Effective Java》作者 Josh Bloch 提倡防护维度说明序列化安全枚举的序列化由 JVM 保证单例不会像普通类那样反序列化时创建新实例反射安全反射不能通过Constructor.newInstance()创建枚举实例线程安全枚举实例的创建由 JVM 保证代码简洁一行代码完成单例定义如果需要绝对防止反射攻击和序列化破坏枚举是实现单例模式的最佳方案。3. 六种实现对比总表实现方式Lazy线程安全锁开销反射安全序列化安全推荐指数懒汉式非线程安全✅❌无❌❌⭐懒汉式线程安全✅✅每次调用❌❌⭐⭐饿汉式❌✅无❌❌⭐⭐⭐⭐双检锁DCL✅✅首次创建❌❌⭐⭐⭐静态内部类✅✅无❌❌⭐⭐⭐⭐⭐枚举❌✅无✅✅⭐⭐⭐⭐⭐选型建议常规项目优先选静态内部类简洁、线程安全、支持延迟加载需要序列化/反射防护用枚举无延迟加载需求且实现越简单越好用饿汉式JDK 自身源码如Runtime就用了饿汉式4. Spring 单例分析4.1 核心区别作用域不同这是经典单例与 Spring 单例最本质的区别对比维度经典单例模式Spring Singleton Bean控制粒度限制在ClassLoader级别一个 JVM 一个实例限制在Spring IoC 容器级别一个容器内一个 Bean ID 一个实例实现方式私有构造方法 静态方法如 DCL、静态内部类属性配置/注解Scope(singleton)单例注册表实例化时机饿汉类加载时懒汉首次调用getInstance()默认容器启动时初始化可Lazy改为懒加载可测试性差构造方法私有难以 Mock 和继承好普通 POJO构造方法公开方便单元测试灵活性一个类在 JVM 中只能有一个实例同一个类可注册为多个不同名的 Bean如多数据源4.2 Spring 的实现机制单例注册表Spring没有使用私有构造方法来限制实例创建而是使用单例注册表Singleton Registry—— 一个ConcurrentHashMap// Spring 内部核心逻辑简化示意publicclassDefaultSingletonBeanRegistry{// 一级缓存存放完整的、可用的单例 BeanprivatefinalMapString,ObjectsingletonObjectsnewConcurrentHashMap(256);publicObjectgetSingleton(StringbeanName,ObjectFactory?singletonFactory){synchronized(this.singletonObjects){ObjectsingletonObjectthis.singletonObjects.get(beanName);if(singletonObjectnull){singletonObjectsingletonFactory.getObject();// 通过反射创建this.singletonObjects.put(beanName,singletonObject);}returnsingletonObject;}}}流程请求 Bean → 查缓存 → 有则直接返回 → 无则通过反射创建 → 放入缓存 → 返回。这意味着 Spring 管理的 Bean 其构造方法通常是public的单例的约束来自IoC 容器的缓存管理而不是 Java 语言层面的访问控制。4.3 三级缓存与循环依赖Spring 在DefaultSingletonBeanRegistry中定义了三级缓存用于解决单例 Bean 之间的循环依赖问题如 A 依赖 BB 依赖 A。三级缓存结构// 一级缓存完全初始化好的 Bean成品privatefinalMapString,ObjectsingletonObjectsnewConcurrentHashMap(256);// 二级缓存早期暴露的半成品 Bean属性尚未注入完毕privatefinalMapString,ObjectearlySingletonObjectsnewConcurrentHashMap(16);// 三级缓存存放 ObjectFactory 工厂用于生成早期 Bean 的代理对象privatefinalMapString,ObjectFactory?singletonFactoriesnewHashMap(16);缓存级别别名存放内容用途一级singletonObjects完整初始化的 Bean 实例最终供客户端使用二级earlySingletonObjects半成品 Bean已实例化未完成属性注入提前暴露给依赖方三级singletonFactoriesObjectFactory工厂产生半成品或 AOP 代理对象解决流程A ↔ B 循环依赖1. 开始创建 A ├─ A 实例化调用构造方法→ 半成品 ├─ 将 A 的 ObjectFactory 放入三级缓存 ├─ 填充属性发现需要 B │ └─ 去缓存找 B → 找不到 → 开始创建 B │ 2. 开始创建 B ├─ B 实例化 → 半成品 ├─ 将 B 的 ObjectFactory 放入三级缓存 ├─ 填充属性发现需要 A │ └─ 去缓存找 A │ ├─ 一级没有 │ ├─ 二级没有 │ └─ 三级找到 A 的 ObjectFactory → 执行 getObject() │ ├─ 如果 A 需要 AOP返回代理对象 │ └─ 否则返回原始半成品 │ → 将结果放入二级缓存从三级缓存移除 │ → B 成功注入 A此时 A 还是半成品 ├─ B 初始化完成 → 放入一级缓存 │ 3. 回到 A ├─ A 成功注入 B ├─ A 初始化完成 → 放入一级缓存 └─ 从二级/三级缓存移除为什么需要三级两级不够关键原因AOP动态代理的生命周期管理。如果只有两级缓存Bean 在实例化后必须马上创建代理对象放入二级缓存但 Spring 的设计原则是 AOP 代理应在 Bean初始化阶段末尾通过BeanPostProcessor创建。三级缓存引入ObjectFactory实现了延迟创建代理没有循环依赖时三级缓存不会被触发AOP 代理按正常生命周期在初始化后创建只有发生循环依赖时依赖方才会触发三级缓存提前生成代理。一句话三级缓存不是必需的AOP 才是三级缓存的原因。4.4Lazy原理Lazy是 Spring 提供的延迟加载机制分为两种场景场景一单独使用ComponentLazypublicclassHeavyService{publicHeavyService(){// 容器启动时不创建首次 getBean 时才创建}}Spring 扫描到Lazy后在容器刷新的最后阶段finishBeanFactoryInitialization会跳过该 Bean 的提前实例化等到代码中第一次调getBean()或注入时才触发创建。场景二注入时使用动态代理ComponentpublicclassAService{AutowiredLazyprivateBServicebService;// B 是懒加载的}此时 A 需要注入 B但 B 又是懒加载不能现在创建Spring 的做法是不为 B 创建真实对象而是用 CGLIB 或 JDK 动态代理生成一个B 的空壳代理对象把这个代理对象注入给 A 的bService字段当 A 执行bService.doSomething()时代理拦截方法调用代理去容器中检查 B 是否已创建若未创建则触发getBean(bService)完成真实创建将方法调用转发给真实的 B 实例这是善意的欺骗——注入的是一个代理壳子把对象的真正创建延迟到了方法首次被调用的瞬间。Lazy解决构造器循环依赖三级缓存只能解决Setter/属性注入的循环依赖。对于构造器注入的循环依赖三级缓存也无力回天因为构造器调用时 Bean 还没实例化放不进三级缓存。此时Lazy可以破局ComponentpublicclassAService{publicAService(LazyBServicebService){// 构造器注入 Lazythis.bServicebService;}}Spring 看到Lazy后直接注入一个 B 的代理壳子到构造器B 本身并不创建。A 成功实例化循环依赖破解。5. 单例模式生态应用单例模式的核心理念——全局唯一、资源共享——在 Java 生态中无处不在。5.1 JDK 源码中的经典单例// Runtime —— 饿汉式单例publicclassRuntime{privatestaticfinalRuntimecurrentRuntimenewRuntime();publicstaticRuntimegetRuntime(){returncurrentRuntime;}privateRuntime(){}}JDK 类实现方式用途Runtime.getRuntime()饿汉式获取 Java 运行时环境Desktop.getDesktop()双检锁DCL获取系统桌面浏览器/邮件System.console()单次检查获取控制台对象5.2 日志框架privatestaticfinalLoggerlogLoggerFactory.getLogger(XxxService.class);LoggerFactory内部通过单例注册表模式维护LoggerContext同一个 Class 对应的Logger实例全局唯一保证日志级别变更能全局同步Logback、Log4j2 均采用此设计5.3 数据库连接池BeanpublicDataSourcedataSource(){returnDataSourceBuilder.create().url(jdbc:mysql://localhost:3306/db).build();}连接池本身HikariCP、Druid在 Spring 容器中是单例 Bean如果不是单例会创建多个连接池耗尽数据库连接资源连接池内部管理的Connection对象则可以有多条池化复用5.4 中间件客户端中间件单例对象原因RedisJedisPool/LettuceConnectionFactory管理网络长连接全局唯一RocketMQDefaultMQProducer与 Broker 搭建复杂路由高代价初始化RabbitMQConnectionFactory维护连接池防止资源浪费ZooKeeperZooKeeper客户端实例维护会话状态全局一致性5.5 Spring 框架内部组件角色ApplicationContext全局唯一的 IoC 容器上下文Environment管理所有配置来源properties、环境变量BeanPostProcessor干预所有 Bean 生命周期的全局处理器6. 总结核心要点单例模式的本质控制一个类在特定范围内的实例数量为 1提供全局访问点经典单例 vs Spring 单例前者在 ClassLoader 级别通过私有构造实现后者在 IoC 容器级别通过注册表缓存实现六种实现推荐顺序静态内部类 ≈ 枚举 饿汉式 DCL 懒汉式同步 懒汉式非同步选型建议场景推荐实现常规 Java 项目静态内部类需要序列化/反射防护枚举无延迟加载需求、追求简单饿汉式JDK 源码场景如 Runtime饿汉式Spring 管理 Bean直接用ComponentScope(singleton)不要手写单例最终一句话在现代 Java 企业级开发中尽量少自己动手写经典单例模式。把对象的生命周期和单例控制交给Spring IoC 容器去管理既能享受单例带来的资源节约又能保持代码的松耦合和高可测性。理解单例模式的价值在于读懂框架源码而不是在业务代码中四处手动实现它。