news 2026/9/7 21:54:05

设计模式01-单例模式:唯一实例的四种实现与选型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设计模式01-单例模式:唯一实例的四种实现与选型

单例模式:高并发下"唯一实例"的四种实现与选型

本文梳理 Java 中单例模式的四种经典实现各自的取舍,以及高并发场景下最容易踩的两个坑。

一、单例在解决什么问题

单例模式(Singleton)回答一个朴素的问题:有些对象,整个系统只需要一个

比如:

  • 配置中心:启动时加载一次,全局共享;
  • 订单号生成器:所有订单号由同一个发号器产出;
  • 数据库连接池:连接资源集中管理,不能各处各建一个池子;
  • 日志收集器:所有模块的日志汇入同一条管道。

它的约束有三条:类只能有一个实例;这个实例由类自己创建;并向全系统提供全局访问入口。

听起来简单,但放进高并发环境,问题立刻变得立体:

挑战具体问题
线程安全多个线程同时创建,如何保证只产生一个实例?
性能加锁之后,每次获取实例都抢锁,吞吐怎么办?
防破坏反射、序列化反序列化能否绕过限制再造一个实例?

Java 社区给出的四种实现,本质上就是在"懒加载、锁开销、安全性"三个维度上做不同的取舍。下面逐一展开。

二、饿汉式:类加载即创建,简单到没有并发问题

最直接的思路:在类加载阶段就把实例创建好。JVM 保证类的静态初始化只执行一次且线程安全(<clinit>方法会加锁),所以压根不存在竞争。

/** * 饿汉式单例 * 场景:系统配置中心——启动必用、全局唯一、全程不变 */publicclassConfigCenter{// 类加载时即完成实例化,JVM 保证线程安全privatestaticfinalConfigCenterINSTANCE=newConfigCenter();// 私有构造器,阻断外部 newprivateConfigCenter(){loadConfig();}publicstaticConfigCentergetInstance(){returnINSTANCE;}publicStringget(Stringkey){return"value-of-"+key;}privatevoidloadConfig(){System.out.println("[ConfigCenter] 配置加载完成(类加载时执行)");}}

取舍分析

  • ✅ 无锁:所有线程读到的是同一个 final 引用,读取没有任何同步开销,高并发下性能最好;
  • ✅ 简单:三行核心代码,几乎不可能写错;
  • ❌ 没有懒加载:类被加载实例就创建,如果实例占用资源大(比如提前建立连接池),而系统运行很久才用到它,就是白白占着内存;
  • ❌ 无法动态初始化:构造器不能带参数,想根据启动参数配置实例就做不到。

适用判断:实例轻量、启动必用的场景——配置管理器、全局常量、工具类。这也是生产中最常见的写法。

三、双重检查锁(DCL):懒加载与性能的平衡

如果实例创建成本高(建立连接、初始化算法参数),又不想启动时就付出这个成本,就需要懒加载:第一次用到才创建。

最朴素的做法是给getInstance整个方法加synchronized,但那样每次获取实例都要抢锁——实例创建好之后,抢锁纯属浪费。

于是有了双重检查锁(Double-Checked Locking):只在实例为空时才进同步块,创建完成后全部无锁

/** * 双重检查锁(DCL)单例 * 场景:订单号生成器——首次生成订单时才初始化雪花算法参数 */publicclassOrderNoGenerator{// volatile 关键:禁止指令重排(下文详解)privatestaticvolatileOrderNoGeneratorINSTANCE;privateOrderNoGenerator(){initSnowflake();}publicstaticOrderNoGeneratorgetInstance(){// 第一重检查:绝大多数调用在这里直接返回,不碰锁if(INSTANCE==null){// 只有创建阶段才加锁synchronized(OrderNoGenerator.class){// 第二重检查:防止多个线程排队等锁后重复创建if(INSTANCE==null){INSTANCE=newOrderNoGenerator();}}}returnINSTANCE;}// 高频调用路径上没有任何锁publiclongnextId(){returnSystem.currentTimeMillis()+100_000L;}privatevoidinitSnowflake(){System.out.println("[OrderNoGenerator] 雪花算法参数初始化(首次 getInstance 时执行)");}}

为什么 volatile 不能省

new OrderNoGenerator()在 JVM 层面不是一步完成的,它至少拆成三步:

  1. 分配对象内存;
  2. 执行构造器初始化;
  3. 把内存地址写入 INSTANCE 引用。

CPU 和 JIT 可能把第 2、3 步重排——先写引用、再初始化。于是可能出现:线程 A 刚执行完第 1、3 步,还没来得及初始化;线程 B 经过第一重检查发现 INSTANCE 非空,直接拿去用,拿到的是一个"半初始化"的对象,后续调用随时可能出错。

volatile禁止了这种重排,保证"引用可见"一定发生在"初始化完成"之后。这也是 DCL 写法里最容易漏掉的一个字——漏掉之后低并发下测不出来,上线后偶发诡异 bug。

取舍分析

  • ✅ 懒加载:首次使用才初始化;
  • ✅ 高性能:创建完成后的调用完全无锁;
  • ❌ 代码复杂:双重检查 + volatile,少一个都不行,容易被"简化"出问题。

适用判断:实例创建成本高、启动不一定用到的场景——ID 生成器、连接池、重量级客户端。

四、静态内部类:借 JVM 之手实现懒加载,还不用自己加锁

有没有一种写法,既有饿汉式的"零锁开销",又有 DCL 的"懒加载",还不需要手动写检查逻辑?

有。思路是:把实例的创建放进一个静态内部类,而 JVM 只在内部类第一次被访问时才加载它

/** * 静态内部类单例 * 场景:本地缓存管理器——懒加载 + 无锁 */publicclassCacheManager{privateCacheManager(){initCache();}// 外部类加载时,Holder 并不会被加载privatestaticclassHolder{// 真正触发加载的是首次访问 Holder.INSTANCE 的那一刻privatestaticfinalCacheManagerINSTANCE=newCacheManager();}publicstaticCacheManagergetInstance(){returnHolder.INSTANCE;}publicObjectget(Stringkey){returnnewObject();// 示意}privatevoidinitCache(){System.out.println("[CacheManager] 缓存初始化(首次 getInstance 时执行)");}}

取舍分析

  • ✅ 懒加载 + 无锁:加载时机由 JVM 类加载机制保证,性能与饿汉式一致;
  • ✅ 优雅:没有 DCL 的双重检查和 volatile,可读性好;
  • ❌ 不能传参:和饿汉式一样,构造器无法接收运行期参数。

适用判断:需要懒加载、但不需要动态参数的场景——缓存管理器、分布式锁客户端、注册中心客户端。

五、枚举:JVM 层面的"绝对唯一"

前面三种实现都挡不住两记冷箭:

  • 反射Constructor.setAccessible(true)强行调用私有构造器;
  • 序列化:把实例序列化到磁盘再反序列化回来,得到一个新对象。

而枚举的实例由 JVM 直接创建,反射调用枚举构造器会抛异常,反序列化也走 JVM 内部机制、不会新建实例。换句话说,防破坏这件事由 JVM 兜底,而不是靠代码自觉

/** * 枚举单例 * 场景:全局事件注册表——对唯一性要求极高 */publicenumGlobalEventRegistry{INSTANCE;GlobalEventRegistry(){initRegistry();}publicvoidpublish(Objectevent){System.out.println("[GlobalEventRegistry] 发布事件: "+event);}publicstaticGlobalEventRegistrygetInstance(){returnINSTANCE;}privatevoidinitRegistry(){System.out.println("[GlobalEventRegistry] 注册表初始化(枚举类加载时执行)");}}

取舍分析

  • ✅ 绝对安全:线程安全由 JVM 保证,反射、序列化两条破坏路径都被封死;
  • ✅ 极简:几行代码;
  • ❌ 无懒加载:枚举类加载时实例即创建。

适用判断:唯一性比启动成本更重要的场景——全局计数器、日志总线、事件注册表。

六、四种实现怎么选

实现懒加载获取实例的锁开销防反射/序列化可否传参初始化一句话定位
饿汉式简单场景的默认选择
DCL创建后无重实例 + 要懒加载
静态内部类懒加载的优雅写法
枚举唯一性至上的终极方案

选型口诀:

  1. 实例轻、启动必用 → 饿汉式;
  2. 实例重、要懒加载、可能要传参 → DCL;
  3. 要懒加载但不用传参、想简单 → 静态内部类;
  4. 唯一性不容有失 → 枚举。

七、两个必须记住的坑

  1. DCL 少了 volatile:表面上看代码没问题,低并发测试也全绿,高并发下会偶发拿到未初始化完成的对象。这是单例话题里最高频的考点,也是线上事故的真实来源;
  2. 把"性能"当作唯一指标:很多团队不管三七二十一上 DCL,其实配置中心这类轻量实例用饿汉式更合适——选型看的是"资源占用 + 启动依赖 + 唯一性要求"的组合,而不是谁的实现更"高级"。

小结

单例的演进线其实是三问:要不要懒加载?能不能接受锁开销?要不要防破坏?四种实现分别回答这三问的不同组合。没有最好的单例,只有最贴合场景的单例。

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

工程发电机租赁怎么定方案?先把负载与进场条件算清

工程临时用电不只是“找一台发电机”。道路、水利、市政和建筑项目的作业面会移动&#xff0c;设备启停有先后&#xff0c;雨季、夜间施工和交叉作业还会改变用电安排。项目方如果只报一个总功率&#xff0c;租来的机组即使铭牌数值看起来够用&#xff0c;也可能在电动机启动、…

作者头像 李华
网站建设 2026/9/7 21:52:11

Python一键转拼音,爽到哭!别再手动查了,这代码太绝了

《有着能够实现汉字转拼音功能的核心代码的程序》, 它是实现汉字转拼音的核心代码程序。有很强的处理能力, 在实现汉字转拼音这方面。在当中有一个常用模块, 它叫做, 是一个强大的中文处理类库, 还是许多开发者处理汉字转拼音常用的工具。本文会详细介绍怎样使用实现汉字转拼音…

作者头像 李华
网站建设 2026/9/7 21:50:21

JSON工厂类设计与性能优化实践

1. JSON工厂类设计背景与核心价值在前后端分离架构成为主流的今天&#xff0c;JSON作为轻量级数据交换格式几乎渗透到每个开发环节。我经历过多个项目从XML到JSON的迁移过程&#xff0c;深刻体会到直接操作原始JSON字符串带来的维护噩梦。一个典型的电商系统中&#xff0c;商品…

作者头像 李华
网站建设 2026/9/7 21:49:33

从零搭建AI视频生产链路:文案、分镜、合成全流程实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 21:48:34

Trae AI编程工具:零基础入门与智能开发实践

1. 项目概述&#xff1a;Trae AI编程入门工具解析 Trae AI是一款面向零基础用户的智能编程学习平台&#xff0c;通过可视化交互和自然语言处理技术降低编程门槛。我最近完整测试了它的教学体系&#xff0c;发现其独特之处在于将传统IDE功能与AI辅助相结合&#xff0c;让完全不懂…

作者头像 李华