当前位置: 首页 > news >正文

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

本文是「设计模式实战解读」系列第一篇。系列文章统一按照定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → 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 #枚举单例 #创建型模式 #软件工程 #面向对象

http://www.jsqmd.com/news/876899/

相关文章:

  • 软考 系统架构设计师之考试感悟5
  • Keil MDK网络组件升级中线程创建失败的解决方案
  • Rizin逆向工程框架:固件分析的七步穿透法与实战避坑指南
  • 百达翡丽全国官方售后中心|四大城市直营门店详细地址与正规维修保养指南 - 资讯纵览
  • 在Taotoken模型广场,如何根据任务类型与预算选择合适的大模型
  • Wand-Enhancer技术深度解析:本地化WeMod增强工具的实现原理与实践指南
  • 2026年5月南京GEO推广公司怎么选,最新榜单 | 首选南京微尚 - 奔跑123
  • 动环监控系统是什么?其主要功能及应用领域有哪些?
  • 学术写作新纪元!2026一站式AI论文写作工具推荐指南
  • 解决Claude Code插件频繁封号与Token不足的Taotoken接入方案
  • 2026肇庆厂房搬迁攻略:设备搬运避坑指南 - 从来都是英雄出少年
  • 为什么你的Windows快捷键突然失灵了?3分钟用Hotkey Detective找出真凶
  • Fiddler HTTPS抓包证书失败全解析:跨平台实战排障指南
  • 大众点评数据采集终极指南:10分钟破解动态字体加密,高效获取全站店铺信息
  • 揭秘Windows热键冲突:Hotkey Detective一键定位占用程序
  • 广州旧金变现怕踩雷?2026年5月福运来等六大平台实测避坑 - 黄金回收
  • qmcdump:3分钟学会的QQ音乐加密文件免费解码终极指南
  • KMS_VL_ALL_AIO:开源智能激活工具让Windows和Office激活变得简单
  • Xournal++:为什么这款开源手写笔记软件能让你的数字工作流焕然一新?
  • 粉尘浓度检测仪采购必看:用户真实评价,奕帆科技口碑好 - 品牌推荐大师
  • 从梯度下降到线搜索:优化算法基础与工程实践全解析
  • Windows 11/10下,Microsoft Store打不开?手把手教你用CheckNetIsolation命令批量解除UWP网络隔离
  • 基于分层高斯过程回归的金属增材制造工艺参数优化
  • 2026张家口市黄金回收诚信靠谱TOP5排行榜 - 亦辰小黄鸭
  • 福州黄金回收指南,福运来全城上门变现更省心 - 黄金回收
  • 2026年4月国内评价高的景观棚生产厂家推荐,景观棚/大型体育场看台/阳台伸缩棚/膜结构雨棚,景观棚公司哪家专业 - 品牌推荐师
  • CVE-2018-1273深度解析:Spring Data Commons SpEL表达式注入漏洞
  • SUWR:首个理论保证无泄漏的局部特征选择方法
  • magic - trace:高分辨率追踪利器,解决应用难题,还能深入洞悉程序运行!
  • 如何利用 Taotoken 的模型广场与统一计费为 AIGC 应用快速迭代提供支持