当前位置: 首页 > 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.zskr.cn/news/1366664.html

相关文章:

  • 2026年最新整理:目前市面上有没有评价很高的英语教学软件推荐?
  • Rust并发安全模式:从线程同步到无锁编程
  • 告别混乱!一张图理清Ubuntu网络管理变迁:从interfaces到Netplan,再到NetworkManager全解析
  • WeChatMsg:如何将微信聊天记录转化为永久数字资产
  • 如何轻松搞定Windows系统依赖:一站式Visual C++运行库完整指南
  • BOTW存档编辑器GUI:Switch平台开源存档修改工具深度解析
  • 终极宽屏体验:如何为《植物大战僵尸》打造专业级宽屏模组
  • Heightmapper完整指南:5分钟免费制作真实3D地形高度图
  • 2026肇庆厂房搬迁攻略:设备搬运避坑指南 - 从来都是英雄出少年
  • 90+格式全支持!ImageGlass:Windows平台最强大的轻量级图像浏览器
  • 基于人工蜂群算法与ANFIS的高维光谱数据特征选择与建模实践
  • Google I/O 2026 收官:Gemini Omni 世界模型 + Gemini 3.5 Flash 全面开放
  • 在Taotoken模型广场,如何根据任务类型与预算选择合适的大模型
  • 构建医疗AI对话系统:基于中文医疗数据集的技术实践指南
  • Python应用敏感配置安全实战:从硬编码到Vault动态注入
  • 高性能桌面管理架构解析:NoFences技术实现深度剖析
  • 2026年腾讯云OpenClaw/Hermes Agent配置Token Plan搭建详细攻略
  • 构建高可用在线机器学习推理系统:分层回退架构设计与金融风控实践
  • 广州旧金变现怕踩雷?2026年5月福运来等六大平台实测避坑 - 黄金回收
  • 机器学习势函数与反向蒙特卡洛在GeO2玻璃中程有序结构建模中的对比研究
  • 独立开发者如何借助Taotoken多模型能力优化个人项目的AI功能模块
  • 机器学习模型在政治文本经济意识形态分类中的性能对比与实战指南
  • Fiddler HTTPS抓包证书失败全解析:跨平台实战排障指南
  • 福州黄金回收指南,福运来全城上门变现更省心 - 黄金回收
  • 2026年横评:16款降AIGC网站横评,这款降AI率效果一骑绝尘!
  • 渗透测试靶场实战指南:从新手到红队工程师的25+靶场进阶路径
  • ComfyUI-Manager终极提速指南:5步解锁多线程下载,让AI模型获取效率提升300%
  • 2026年论文AI率爆表别慌!毕业生实测10个降AI率工具,谁是真神器?内附免费降AI率干货 - 降AI实验室
  • 2026广东职称评审机构排名推荐哪个好? - 资讯纵览
  • 佛山黄金回收靠谱之选,福运来免费上门足不出户安心变现 - 黄金回收