news 2026/5/24 11:07:24

设计模式实战解读(一):单例模式——全局唯一实例的正确打开方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设计模式实战解读(一):单例模式——全局唯一实例的正确打开方式

本文是「设计模式实战解读」系列第一篇。系列文章统一按照定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ的结构展开,每篇聚焦一个模式讲透。


一句话定义

单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。

归属:创建型模式。


一、没有单例时的痛点

假设你正在做一个配置管理模块,系统启动时需要从 Nacos/Apollo 加载配置并缓存到内存里:

// 问题代码:每次需要配置时都 new 一个ConfigManagerconfigA=newConfigManager();// 加载一次远程配置ConfigManagerconfigB=newConfigManager();// 又加载一次远程配置// configA 和 configB 是两个独立实例// 1. 重复加载浪费网络 IO// 2. 两份缓存不一致(A 修改了配置,B 看不到)// 3. 如果配置里有状态(如 version),两份实例会分裂

类似的痛点还出现在:数据库连接池、线程池管理器、日志打印器、ID 生成器——这些组件如果被 new 多份,要么浪费资源,要么产生不一致的行为。

核心诉求:全局只需要一份,任何地方拿到的都是同一个。


二、模式结构

┌──────────────────────────────┐ │ Singleton │ ├──────────────────────────────┤ │ - instance: Singleton │ ← 唯一实例(静态字段) ├──────────────────────────────┤ │ - Singleton() │ ← 私有构造(禁止外部 new) │ + getInstance(): Singleton │ ← 全局访问点 │ + businessMethod() │ ← 业务方法 └──────────────────────────────┘

三要素:

  1. 私有构造函数——禁止外部new
  2. 静态实例字段——类级别持有唯一实例
  3. 公开静态方法——全局获取入口

三、核心实现(五种写法对比)

3.1 饿汉式(推荐在大多数场景使用)

publicclassSingleton{// 类加载时就创建实例(JVM 保证线程安全)privatestaticfinalSingletonINSTANCE=newSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returnINSTANCE;}}

优点:实现简单,线程安全,无同步开销。
缺点:类加载时就创建,如果实例很重且未必被使用,造成浪费。
适用:实例创建成本低、确定会被使用的场景。

3.2 懒汉式 + 双重检查锁(DCL)

publicclassSingleton{// volatile 防止指令重排序privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){// 第一次检查(无锁)synchronized(Singleton.class){// 加锁if(instance==null){// 第二次检查instance=newSingleton();}}}returninstance;}}

优点:懒加载 + 线程安全 + 锁粒度小。
缺点:代码稍复杂,volatile 有轻微性能开销。
适用:实例创建成本高、不确定是否会被使用。

为什么需要 volatile?因为instance = new Singleton()不是原子操作,JVM 可能先分配内存、再赋值引用、最后执行构造函数(指令重排)。不加 volatile,其他线程可能拿到一个"半初始化"的实例。

3.3 静态内部类(推荐的懒加载方案)

publicclassSingleton{privateSingleton(){}// 内部类在第一次被引用时才加载(JVM 保证线程安全)privatestaticclassHolder{privatestaticfinalSingletonINSTANCE=newSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}}

优点:懒加载 + 线程安全 + 无同步开销 + 代码简洁。
缺点:无法传参初始化。
适用:大多数需要懒加载的场景。这是实际项目中最推荐的写法。

3.4 枚举单例(最安全的写法)

publicenumSingleton{INSTANCE;privatefinalAtomicLongcounter=newAtomicLong(0);publiclongnextId(){returncounter.incrementAndGet();}}// 使用longid=Singleton.INSTANCE.nextId();

优点:天然防反射、防序列化破坏、代码极简、线程安全。
缺点:不能继承其他类(枚举隐式 extends Enum)、无法懒加载。
适用:对安全性要求极高、防止反射攻击的场景。Effective Java 推荐的写法。

3.5 五种写法对比

写法线程安全懒加载防反射防序列化代码复杂度
饿汉式
DCL
静态内部类
枚举最低
容器管理 (Spring)N/AN/A零(框架做)

四、真实应用场景

4.1 框架级应用

Spring IoC 容器:Spring Bean 默认scope=singleton。整个容器中同一个 BeanDefinition 只有一个实例。这不是 GoF 单例(不是类级别唯一),而是容器级别唯一——但核心思想一致。

Runtime.getRuntime():JDK 标准库中的经典饿汉单例。

Slf4j LoggerFactory:每个类获取的 Logger 实例在内部是缓存的,同一个 name 返回同一个实例。

4.2 业务级应用

业务场景单例对象为什么用单例
数据库连接池HikariDataSource多份连接池浪费连接资源
分布式 ID 生成Snowflake Worker全局唯一 workerId 保证不重复
配置中心客户端NacosConfigManager只需一份缓存,变更统一监听
本地缓存Caffeine Cache缓存命中率依赖数据集中在一处
限流器RateLimiter全局统一计数才能准确限流
线程池ThreadPoolExecutor多份线程池破坏全局资源控制

4.3 iPaaS 场景中的典型单例

在流程引擎类项目中,以下组件适合用单例:

  • FlowOrchestrator(流程编排器):编排逻辑无状态,全局一个实例即可
  • InterruptSignalCache(中断信号缓存):全局 Guava Cache,所有流程共享
  • ExecutionMetrics(执行指标收集):全局计数器,汇总后推送到监控系统
  • SnowflakeIdGenerator(ID 生成器):基于 workerId 的全局唯一实例

五、常见变种

5.1 多例模式(Multiton)

有时不是"全局只要一个",而是"某个 key 对应一个"。比如按租户 ID 隔离的缓存实例:

publicclassTenantCache{privatestaticfinalMap<String,TenantCache>INSTANCES=newConcurrentHashMap<>();privateTenantCache(StringtenantId){// 初始化该租户的缓存}publicstaticTenantCachegetInstance(StringtenantId){returnINSTANCES.computeIfAbsent(tenantId,TenantCache::new);}}

5.2 可销毁单例

某些场景下(热加载、测试隔离)需要销毁后重建单例:

publicclassReloadableSingleton{privatestaticvolatileReloadableSingletoninstance;publicstaticvoiddestroy(){instance=null;// 销毁}publicstaticReloadableSingletongetInstance(){if(instance==null){synchronized(ReloadableSingleton.class){if(instance==null){instance=newReloadableSingleton();}}}returninstance;}}

5.3 线程级单例(ThreadLocal)

全局唯一不是诉求,线程内唯一才是:

publicclassThreadLocalSingleton{privatestaticfinalThreadLocal<ThreadLocalSingleton>INSTANCE=ThreadLocal.withInitial(ThreadLocalSingleton::new);publicstaticThreadLocalSingletongetInstance(){returnINSTANCE.get();}}

典型场景:JDBC Connection(线程内复用,线程间隔离)、RequestContext。


六、优缺点

优点缺点
全局唯一,避免重复创建隐藏了类之间的依赖关系
共享资源的统一管控对单元测试不友好(全局状态难 mock)
延迟初始化节省资源违反单一职责(既管创建又管业务)
提供全局访问点多线程场景容易踩坑

七、避坑指南

坑 1:反射攻击破坏单例

// 恶意代码通过反射绕过私有构造Constructor<Singleton>c=Singleton.class.getDeclaredConstructor();c.setAccessible(true);Singletonanother=c.newInstance();// 第二个实例!

防御:在构造函数里加校验:

privateSingleton(){if(INSTANCE!=null){thrownewIllegalStateException("Singleton already initialized");}}

或者直接用枚举单例(JVM 禁止反射创建枚举实例)。

坑 2:序列化/反序列化破坏单例

实现了 Serializable 的单例,反序列化时会创建新实例。

防御:添加readResolve()方法:

privateObjectreadResolve(){returnINSTANCE;// 反序列化时返回已有实例}

坑 3:Spring 中误用 prototype scope

Spring Bean 默认是 singleton,但如果一个 singleton Bean 注入了一个 prototype Bean,prototype 不会每次都新建——因为注入只发生一次。

防御:用@Lookup注解或ObjectFactory<T>来获取 prototype Bean。

坑 4:单例持有可变状态导致线程安全问题

单例本身是安全的,但如果它持有可变状态(如 HashMap),多线程并发读写会出问题。

防御:单例的内部字段要么不可变(final),要么用线程安全容器(ConcurrentHashMap、AtomicLong)。

坑 5:类加载器隔离导致"多个单例"

在 Tomcat 等容器中,不同 ClassLoader 会各自加载一份类——导致看似是单例,实际有多个实例。

防御:确保单例类在 parent ClassLoader 中加载,或者用容器提供的单例管理机制。


八、常见问题(FAQ)

Q:Spring 的 Bean 是单例模式吗?

A:Spring 的 singleton scope 是容器级别的唯一(每个 ApplicationContext 维护一份),不是 GoF 意义上的类级别唯一。一个类在多个 ApplicationContext 中可以有多个实例。但在业务代码中效果等同于单例,因为通常只有一个容器。

Q:单例和静态类(工具类)有什么区别?

A:静态类不能实现接口、不能被 mock、不能被 Spring 管理、不能做延迟初始化。单例是一个"对象",可以实现接口、可以被注入、可以多态。如果组件需要面向接口编程或被测试框架 mock,用单例;如果纯粹是无状态的工具方法,用静态类。

Q:微服务时代还需要单例吗?

A:需要。微服务让进程级别的"全局"范围变小了(从整个系统缩小到单个服务内),但单个服务内依然有"全局唯一"的诉求——连接池、缓存、配置客户端、ID 生成器。单例的适用范围从不跨进程边界。

Q:什么情况下不应该用单例?

A:① 对象持有大量请求级别的状态(应该每次 new);② 对象需要在测试中被频繁替换(应该用依赖注入);③ 对象的生命周期比进程短(如用户会话级对象)。

Q:DCL 中 volatile 能不能省略?

A:不能。省略 volatile 会导致指令重排序问题——线程 A 可能观察到 instance 非 null,但实例还未完成构造函数的初始化。这在高并发下是真实的 bug,JDK 5+ 的 volatile 语义才修复了这个问题。


九、小结

单例模式是最简单的设计模式,也是最容易用错的。核心记住三点:

  1. 优先用静态内部类或枚举,不要写 DCL 除非有充分理由
  2. Spring 项目里直接用 @Component + @Autowired,让框架管单例
  3. 单例内部状态必须线程安全——这是 90% 单例 bug 的来源

下一篇我们聊工厂模式——当对象创建变得复杂时,如何把"创建逻辑"从业务代码中解耦出来。


标签:#设计模式 #单例模式 #Singleton #Java #Spring #线程安全 #DCL #volatile #枚举单例 #创建型模式 #软件工程 #面向对象

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/24 10:58:19

KMS_VL_ALL_AIO:开源智能激活工具让Windows和Office激活变得简单

KMS_VL_ALL_AIO&#xff1a;开源智能激活工具让Windows和Office激活变得简单 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为Windows系统未激活的水印烦恼吗&#xff1f;Office软件频繁弹…

作者头像 李华
网站建设 2026/5/24 10:57:29

基于Transformer的科研评审报告多标签分类:从BERT到SPECTER2的工程实践

1. 项目概述与核心价值 在科研基金评审这个决定资源分配的关键环节&#xff0c;评审报告的质量与一致性一直是学界和资助机构关注的焦点。传统的评审分析依赖人工阅读和定性归纳&#xff0c;面对成千上万份报告时&#xff0c;不仅效率低下&#xff0c;也难以进行大规模、标准化…

作者头像 李华
网站建设 2026/5/24 10:55:23

15分钟快速上手Atmosphere大气层:Switch游戏体验终极优化指南

15分钟快速上手Atmosphere大气层&#xff1a;Switch游戏体验终极优化指南 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable 想要彻底释放你的Nintendo Switch游戏性能吗&#xff1f;Atmosphe…

作者头像 李华
网站建设 2026/5/24 10:52:25

DLSS Swapper:告别游戏卡顿,一键升级DLSS的智能管家

DLSS Swapper&#xff1a;告别游戏卡顿&#xff0c;一键升级DLSS的智能管家 【免费下载链接】dlss-swapper 项目地址: https://gitcode.com/GitHub_Trending/dl/dlss-swapper 你是否曾经在畅玩最新3A大作时&#xff0c;明明配备了高端RTX显卡&#xff0c;却依然被不稳定…

作者头像 李华