关于面向对象设计模式中单例模式的笔记
何为单例模式?
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类在整个 JVM 中只有一个实例,并提供一个全局访问点来获取这个实例。
它的核心特征有三个:
- 私有构造器:禁止外部通过 new 创建实例
- 私有静态实例:类内部自己持有唯一实例
- 公有静态方法:对外提供获取该实例的入口
它为何存在?解决了什么问题?
单例模式的存在,本质上是解决"唯一性"和"受控访问"这两类问题:
- 控制资源唯一性
某些资源在系统中物理上或逻辑上只应存在一份。比如:- 一个数据库连接池(多份会导致连接数失控)
- 一个线程池
- 一个全局配置对象
- 一个日志写入器(多份会导致日志文件被并发写乱)
- 提供全局访问点
有些对象需要被系统各处频繁使用(配置、注册表、缓存)。单例提供统一的 getInstance() 入口,避免到处传递引用,也避免用一堆 static 变量散落各处。 - 节省内存与创建开销
对于创建成本高、无状态或状态可共享的对象(如重量级的工厂、解析器),只创建一次可以避免重复开销。 - 保证状态一致性
当多个组件需要读写同一份共享状态时(如全局计数器、缓存),单例确保大家操作的是同一个对象,避免"各持一份副本导致数据不一致"。
主要用在哪些方面?
配置管理:全局 Config 对象,一次加载、处处读取
日志器:统一的 Logger,避免多实例争抢文件句柄
连接池/线程池:数据库连接池、HTTP 连接池、线程池管理器
缓存:本地缓存(如 Guava Cache 的单例封装)
注册表/管理器:Spring 容器中的 Bean 默认就是单例作用域
设备/硬件访问:打印机后台服务、串口、显卡驱动封装
对话框/窗口:GUI 中某些只能存在一个的窗口
Spring 框架:@Service、@Component 默认 singleton scope,是单例思想最广泛的应用
注意:Spring 的单例是容器级单例(每个容器一个),不是 JVM 级单例,这是它的一个重要区别。
如果没有单例模式,会导致什么后果?
- 资源耗尽或冲突
连接池多实例:每个 new 一个池,各自持有连接,数据库连接数迅速打满,导致 Too many connections。
日志多实例:多个 Logger 同时写同一文件,日志交错、丢失甚至文件损坏。
线程池多实例:线程数失控,CPU 和内存被拖垮。 - 状态不一致
全局配置被多个实例持有,A 处修改了配置,B 处读到的还是旧值,产生难以排查的 bug。计数器、缓存同理,出现"数据对不上"的诡异现象。 - 重复创建的性能开销
重量级对象被反复创建销毁,GC 压力增大,启动和运行变慢。 - 无法保证唯一性语义
业务上要求"系统中只能有一个 X"(如唯一的任务调度器、唯一的 ID 生成器),没有单例就只能靠约定和人工检查,极易被破坏,且难以在编译期发现。 - 全局访问困难
要么到处传引用(污染方法签名),要么用大量 public static 变量——后者无法懒加载、无法控制初始化顺序、无法面向接口替换,可测试性和可维护性都差。 - 并发下的隐患
若无单例而用普通静态变量/多实例模拟,容易出现竞态条件,多个实例同时修改共享资源而无统一锁保护。
总结
单例模式 = 私有构造 + 唯一实例 + 全局访问点。 它解决的是"资源唯一性、全局受控访问、状态一致性、创建开销"四类问题,广泛用于配置、日志、连接池、缓存、Spring Bean 等场景。没有它,会导致资源失控、状态不一致、性能浪费和唯一性语义无法保证。但它不是银弹——过度使用会带来隐藏依赖和可测试性问题,应结合依赖注入谨慎使用。
案例
假设我们需要一个LocalCacheService,负责缓存热点数据(商品信息、用户信息等)。多个 Service 都要用它读写缓存。它的关键需求在于 全系统共享同一份缓存数据(否则 A 写入、B 读不到) ,避免缓存被多份实例各存一份,浪费内存且数据不一致
案例1:无手写单例,无@Component的反面案例
LocalCacheService类:
// 缓存服务:普通类,没有任何单例措施
public class LocalCacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
}
OrderService类
// 订单服务:自己 new 了一个缓存
public class OrderService {
private final LocalCacheService cache = new LocalCacheService(); // 自己造一个
public void cacheOrder(String orderId, Object order) {
cache.put("order:" + orderId, order);
}
}
ProductService类
// 商品服务:也自己 new 了一个缓存
public class ProductService {
private final LocalCacheService cache = new LocalCacheService(); // 又造一个
public Object getOrderFromCache(String orderId) {
return cache.get("order:" + orderId); // 永远返回 null!
}
}
运行结果
无论如何,ProductService.getOrderFromCache()只会返回null
案例2:有手写单例,无 @Component
LocalCacheService类:
// 手写单例的缓存服务
public class LocalCacheService {
// 1. 私有静态实例(饿汉式,类加载即创建)
private static final LocalCacheService INSTANCE = new LocalCacheService();
private final Map<String, Object> cache = new ConcurrentHashMap<>();
// 2. 私有构造器,禁止外部 new
private LocalCacheService() {}
// 3. 公有静态方法,全局访问点
public static LocalCacheService getInstance() {
return INSTANCE;
}
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
}
OrderService类
public class OrderService {
public void cacheOrder(String orderId, Object order) {
LocalCacheService.getInstance().put("order:" + orderId, order); // 拿全局唯一实例
}
}
ProductService类
public class ProductService {
public Object getOrderFromCache(String orderId) {
return LocalCacheService.getInstance().get("order:" + orderId); // 拿到同一份缓存
}
}
运行结果
调用orderService.cacheOrder("1","111"); 和 Object orderFromCache = productService.getOrderFromCache("1"); 后,返回结果为 “111”
优点
- 强唯一性保证 :私有构造器 + 静态实例,JVM 级别强制唯一,不依赖容器
- 懒加载可控(懒汉式/静态内部类) :可控制实例化时机,避免启动时无谓创建
- 无框架依赖 :不引入 Spring 也能用,适合工具类库、SDK、独立模块
- 访问直接 :getInstance() 随处可调,无需注入
- 性能开销小 :饿汉式无锁;静态内部类无锁懒加载
缺点
- 隐藏依赖 :OrderService 直接调 getInstance(),依赖关系不写在构造器/字段上,从签名看不出它依赖缓存,难以理解
- 难以测试 :想给 OrderService 注入一个 Mock 缓存?做不到——它硬编码了 getInstance(),只能测真实单例
- 有状态单例线程安全需自理 :单例只保证唯一,ConcurrentHashMap 是你自己选的;若换成 HashMap,并发下就出问题
- 反射/序列化可破坏 :普通实现可被反射 setAccessible(true) 造出第二个实例(枚举实现可防)
- 多 ClassLoader / 多 JVM 不唯一 :不同类加载器各有一份;多机部署时每台机器各一个,跨机器不共享
- 初始化顺序难控 :饿汉式在类加载时创建,若构造器依赖其他尚未初始化的资源,可能出问题
适用场景:SDK、工具库、无 Spring 环境的独立应用、需要严格 JVM 级唯一的组件。
案例3:无手写单例,有 @Component
LocalCacheService类:
// 交给 Spring 容器管理,默认就是 singleton 作用域
@Component
public class LocalCacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
}
OrderService类
@Service
public class OrderService {
private final LocalCacheService cacheService;
// 构造器注入:依赖关系显式声明
public OrderService(LocalCacheService cacheService) {
this.cacheService = cacheService;
}
public void cacheOrder(String orderId, Object order) {
cacheService.put("order:" + orderId, order);
}
}
ProductService类
@Service
public class ProductService {
private final LocalCacheService cacheService;
public ProductService(LocalCacheService cacheService) {
this.cacheService = cacheService;
}
public Object getOrderFromCache(String orderId) {
return cacheService.get("order:" + orderId); // 和 OrderService 用的是同一个实例
}
}
运行结果
调用orderService.cacheOrder("1","111"); 和 Object orderFromCache = productService.getOrderFromCache("1"); 后,返回结果为 “111”
优点
- 依赖显式化 :构造器注入,从类签名就能看出依赖谁,可读性好
- 极易测试 :单测时直接 new OrderService(mockCache),无需任何框架
- 生命周期由容器管理 :创建、销毁、初始化回调都由 Spring 统一处理,无手动管理负担
- 天然支持 AOP :事务、日志、缓存注解等切面可无缝织入
- 可替换、可配置 :面向接口注入,换实现只改配置;可通过 @Scope 灵活改作用域
- 无反射破坏风险 :不暴露静态 getInstance(),外部无法绕过容器造实例(容器外 new 的对象不归容器管,但业务代码不会这么做)
- 与生态整合 :和 Spring 的事务、安全、监控等基础设施天然协同
缺点
- 依赖 Spring 容器 :脱离 Spring 环境(纯工具库、SDK、非 Spring 应用)无法使用
- 单例范围是容器级 :多个 ApplicationContext(如多容器测试)会各有一份,不是 JVM 级唯一
- 单例 ≠ 线程安全 :容器只保证唯一,Bean 内有可变成员变量时仍需自己保证线程安全(推荐无状态 Bean)
- 隐式约定 :默认 singleton 是"约定",新人可能误以为每次注入都是新对象,或误在单例里放请求级状态导致串数据
- 容器外 new 不受控 :如果有人在容器外 new LocalCacheService(),会得到第二个实例,容器管不到(但规范开发下不会这么做)
适用场景:绝大多数 Spring 企业级业务开发——这是最主流、最推荐的落地方式。
作者:RCCS_1154
出处:http://www.cnblogs.com/RCCS/
本文版权归作者和博客园共有,转载必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。

浙公网安备 33010602011771号