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

设计模式:单例模式

设计模式:单例模式

目录

  • 单例模式是什么
  • 为什么需要单例
  • 五种写法全览
  • 为什么 DCL 必须加 volatile
  • 实际应用场景
  • 小结

单例模式是什么

单例模式是后端面试中的一个热点:一个类在整个应用中只能有一个实例,并提供一个全局访问点。

单例模式要做的就是这件事:用代码保证某个对象全局唯一,谁来拿都是同一个。

为什么需要单例

假设你写了一个数据库连接池管理器,每个模块各自new了一个实例:

// 订单模块ConnectionPoolpoolA=newConnectionPool();// 用户模块ConnectionPoolpoolB=newConnectionPool();// 支付模块ConnectionPoolpoolC=newConnectionPool();

三个模块各自初始化了一个连接池,数据库连接数直接翻了三倍。更严重的是,它们各自维护自己的连接状态,互相不知道对方用了多少连接,连接泄漏了也没法统一排查,。

多个实例意味着资源被浪费、状态不一致、管理失控。连接池、线程池、配置中心、日志记录器,这些东西天然就应该是全局唯一的。单例模式就是用代码强制保证这种唯一性。

五种写法全览

写法一:饿汉式

最直白的写法:类加载的时候就创建实例,不管你用不用。

publicclassSingleton{// 类加载时就创建实例privatestaticfinalSingletonINSTANCE=newSingleton();// 私有构造,禁止外部 newprivateSingleton(){}// 全局访问点publicstaticSingletongetInstance(){returnINSTANCE;}}

如何做到线程安全? JVM 的类加载机制保证了static字段的初始化只会执行一次,这个过程是线程安全的。我们在使用的时候就不需要加锁,因为JVM 帮我们兜底了。

缺点也很明显:不管你用不用,类一加载实例就创建好了。如果这个对象很重(比如初始化时要加载大量配置文件),而应用启动后可能很长时间都用不到它,那这块资源就会被一直白白占着。

写法二:懒汉式

为了解决"用不到就别创建"的问题,延迟到第一次调用getInstance()时再创建:

publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}}

看起来没问题,但多线程下会出事。两个线程同时调getInstance(),都判断instance == null,于是各自创建了一个实例,单例就被打破了。

写法三:懒汉式 + synchronized

加锁,让同一时刻只有一个线程能进入创建逻辑:

publicclassSingleton{privatestaticSingletoninstance;privateSingleton(){}publicstaticsynchronizedSingletongetInstance(){if(instance==null){instance=newSingleton();}returninstance;}}

线程安全了,但性能差。synchronized加在方法上,意味着每次调用getInstance()都要拿锁,哪怕实例早就创建好了,读操作也要排队。在高并发场景下,这里会成为瓶颈。

写法四:双重检查锁(DCL)

既然只有第一次创建时需要加锁,后续读取不需要,那我们可以把锁的粒度缩小:

publicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){// 第一次检查:不加锁synchronized(Singleton.class){if(instance==null){// 第二次检查:加锁后再确认instance=newSingleton();}}}returninstance;}}

第一个if是为了性能:实例已经创建了,直接返回,不用排队抢锁。

第二个if是为了安全:可能有两个线程同时通过了第一个if,进入synchronized块后,必须再确认一次,避免重复创建。

关键点:instance必须用volatile修饰。这一点我们在后文中单独拎出来讲。

写法五:静态内部类

这是兼顾延迟加载和线程安全的最优写法之一:

publicclassSingleton{privateSingleton(){}// 静态内部类,只有在被引用时才会加载privatestaticclassHolder{privatestaticfinalSingletonINSTANCE=newSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}}

原理:Java 的内部类只有在真正被使用时才会被类加载器加载。当getInstance()第一次被调用时,Holder类才会初始化,INSTANCE才会被创建。在此之前,Holder类根本不存在于内存中。

线程安全同样由 JVM 的类加载机制保证,不需要synchronized,不需要volatile,代码简洁,性能最优。

写法六:枚举

《Effective Java》作者 Joshua Bloch 最推荐的写法:

publicenumSingleton{INSTANCE;publicvoiddoSomething(){System.out.println("执行业务逻辑");}}

就这么多代码。枚举天然是单例的,JVM 保证枚举实例的唯一性。更重要的是,枚举能防御反射和序列化攻击,这是其他写法都做不到的。

为什么 DCL 必须加 volatile

问题出在instance = new Singleton()这一行。maybe你以为它是一个原子操作,实际上 JVM 执行它分了三步:

1. 分配内存空间 2. 初始化对象(执行构造方法) 3. 将 instance 指向分配的内存

如果没有volatile,JVM 可能会进行指令重排序,把步骤 2 和步骤 3 调换顺序:

1. 分配内存空间 2. 将 instance 指向分配的内存 ← 先指过去了 3. 初始化对象 ← 但对象还没初始化完

这会导致什么问题?线程 A 执行到第 2 步,instance已经不是null了,但对象还没初始化完成。此时线程 B 调用getInstance(),第一个if判断instance == nullfalse,直接返回了一个尚未初始化完成的对象。用这个对象去调方法,轻则数据错误,重则直接NullPointerException

volatile用来禁止指令重排序。加了volatile之后,JVM 会保证instance = new Singleton()的三步严格按顺序执行,不会出现"指针先到位、对象还没好"的情况。

实际应用场景

JDK 中的单例:Runtime.getRuntime()就是一个经典的饿汉式单例。每个 Java 应用只有一个 Runtime 实例,通过它获取 JVM 内存信息、执行系统命令。Desktop.getDesktop()也是单例,用于打开浏览器、邮件客户端等桌面操作。

Spring Bean:Spring 容器中的 Bean 默认就是单例模式。你在代码里注入的@Autowired UserService userService,整个应用里拿到的都是同一个实例。Spring 通过 IoC 容器管理 Bean 的生命周期,本质上就是一个单例注册表。你可以通过@Scope("prototype")改成多例,但绝大多数 Service、DAO 都是单例,因为它们本身无状态,单例既节省资源又不影响并发。

数据库连接池:Druid 等连接池在应用中通常只有一个实例,全局共享,统一管理连接的创建、回收、监控。

小结

单例模式的核心:保证一个类全局只有一个实例。但围绕这个简单的目标,衍生出了线程安全、延迟加载、指令重排序等一系列问题。每种写法都是在这些维度之间做取舍,没有完美的方案,只有最适合你场景的方案。

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

相关文章:

  • Java毕业设计-基于 Spring Boot 的房屋交易管理系统的设计与实现 基于 Spring Boot 的线上房产交易服务平台(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • MediaPipe GPU加速实战指南:从零配置到性能调优
  • 闲置大牌包怎么卖高价?2026 成都回收实测,禹竞名奢汇连锁直营实测分享 - 奢品小当家
  • 深入解析UART通信:从FIFO、流控制到中断优化实战
  • 2026年卫生间隔断配件深度选型:不同需求下的选择路径 - 信息热点
  • 2026年光纤收发器厂家选型指南:代表性品牌解析与高性价比方案推荐 - 信息热点
  • I VISTA 官方介绍|泛娱乐出海全链路技术服务商|I VISTA 官方对接指南 - 互联网科技品牌测评
  • Boss-Key终极指南:Windows隐私保护神器,一键隐藏窗口的完整解决方案
  • 超快恢复二极管1N6536:30ns反向恢复时间在高频开关电源中的应用与选型
  • 如何用WeChatMsg打造个人AI记忆库:微信聊天记录永久保存终极指南
  • 2026年婴儿床铃值不值得选:五家优选品牌深度解析 - 科技焦点
  • 济南适己化全屋定制——原息柏木 vs 颗粒板 vs 欧松板 vs 多层板,到底怎么选? - 济南原息康养定制
  • 2026深圳二手中央空调出售公司 实测测评 - LYL仔仔
  • Electron跨平台音乐聚合播放器架构深度解析与实战指南
  • 三层交换机原理与华为实战配置:从VLAN间路由到核心网络部署
  • 2026上海家庭聚餐私厨上门公司 实测盘点本地五家对比 - LYL仔仔
  • 破解供水设备价格错配:双核三全方法论如何平衡价值与成本? - 资讯快报
  • 电力MOSFET:从结构原理到高频开关应用的深度解析
  • 如何高效使用Adobe Illustrator脚本自动化:提升设计工作流的完整指南
  • 卫生间隔断配件常见问题解答(2026专家版) - 信息热点
  • 从原理到实践:在Unreal中构建基于波叠加的动态水面材质
  • 2026成都黄金行业研判:高价周期下个人售金最优方案 - 奢侈品回收评测
  • 文档图像机器翻译技术:挑战、突破与应用
  • 上海黄金回收正规渠道怎么选?本地门店实测干货指南 - 开心测评
  • 少儿书画大赛线上票选怎么做?微信投票详细教程 - 微信投票小程序
  • JenOS RTOS:JN516x无线MCU低功耗物联网开发实战指南
  • 破解供水设备价格误区:3C场景适配定价法如何实现高性价比? - 资讯快报
  • 一人公司如何用WorkBuddy搭工作流,完整演示纯干货
  • 嵌入式Linux内核调试实战:JTAG与CodeWarrior深度应用指南
  • DSP56800E命令行调试器核心命令详解:寄存器与内存操作实战