关于面向对象设计模式中单例模式的笔记

何为单例模式?

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类在整个 JVM 中只有一个实例,并提供一个全局访问点来获取这个实例。

它的核心特征有三个:

  1. 私有构造器:禁止外部通过 new 创建实例
  2. 私有静态实例:类内部自己持有唯一实例
  3. 公有静态方法:对外提供获取该实例的入口

它为何存在?解决了什么问题?

单例模式的存在,本质上是解决"唯一性"和"受控访问"这两类问题:

  1. 控制资源唯一性
    某些资源在系统中物理上或逻辑上只应存在一份。比如:
    • 一个数据库连接池(多份会导致连接数失控)
    • 一个线程池
    • 一个全局配置对象
    • 一个日志写入器(多份会导致日志文件被并发写乱)
  2. 提供全局访问点
    有些对象需要被系统各处频繁使用(配置、注册表、缓存)。单例提供统一的 getInstance() 入口,避免到处传递引用,也避免用一堆 static 变量散落各处。
  3. 节省内存与创建开销
    对于创建成本高、无状态或状态可共享的对象(如重量级的工厂、解析器),只创建一次可以避免重复开销。
  4. 保证状态一致性
    当多个组件需要读写同一份共享状态时(如全局计数器、缓存),单例确保大家操作的是同一个对象,避免"各持一份副本导致数据不一致"。

主要用在哪些方面?

配置管理:全局 Config 对象,一次加载、处处读取

日志器:统一的 Logger,避免多实例争抢文件句柄

连接池/线程池:数据库连接池、HTTP 连接池、线程池管理器

缓存:本地缓存(如 Guava Cache 的单例封装)

注册表/管理器:Spring 容器中的 Bean 默认就是单例作用域

设备/硬件访问:打印机后台服务、串口、显卡驱动封装

对话框/窗口:GUI 中某些只能存在一个的窗口

Spring 框架:@Service、@Component 默认 singleton scope,是单例思想最广泛的应用

注意:Spring 的单例是容器级单例(每个容器一个),不是 JVM 级单例,这是它的一个重要区别。

如果没有单例模式,会导致什么后果?

  1. 资源耗尽或冲突
    连接池多实例:每个 new 一个池,各自持有连接,数据库连接数迅速打满,导致 Too many connections。
    日志多实例:多个 Logger 同时写同一文件,日志交错、丢失甚至文件损坏。
    线程池多实例:线程数失控,CPU 和内存被拖垮。
  2. 状态不一致
    全局配置被多个实例持有,A 处修改了配置,B 处读到的还是旧值,产生难以排查的 bug。计数器、缓存同理,出现"数据对不上"的诡异现象。
  3. 重复创建的性能开销
    重量级对象被反复创建销毁,GC 压力增大,启动和运行变慢。
  4. 无法保证唯一性语义
    业务上要求"系统中只能有一个 X"(如唯一的任务调度器、唯一的 ID 生成器),没有单例就只能靠约定和人工检查,极易被破坏,且难以在编译期发现。
  5. 全局访问困难
    要么到处传引用(污染方法签名),要么用大量 public static 变量——后者无法懒加载、无法控制初始化顺序、无法面向接口替换,可测试性和可维护性都差。
  6. 并发下的隐患
    若无单例而用普通静态变量/多实例模拟,容易出现竞态条件,多个实例同时修改共享资源而无统一锁保护。

总结

单例模式 = 私有构造 + 唯一实例 + 全局访问点。 它解决的是"资源唯一性、全局受控访问、状态一致性、创建开销"四类问题,广泛用于配置、日志、连接池、缓存、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”

优点

  1. 强唯一性保证 :私有构造器 + 静态实例,JVM 级别强制唯一,不依赖容器
  2. 懒加载可控(懒汉式/静态内部类) :可控制实例化时机,避免启动时无谓创建
  3. 无框架依赖 :不引入 Spring 也能用,适合工具类库、SDK、独立模块
  4. 访问直接 :getInstance() 随处可调,无需注入
  5. 性能开销小 :饿汉式无锁;静态内部类无锁懒加载

缺点

  1. 隐藏依赖 :OrderService 直接调 getInstance(),依赖关系不写在构造器/字段上,从签名看不出它依赖缓存,难以理解
  2. 难以测试 :想给 OrderService 注入一个 Mock 缓存?做不到——它硬编码了 getInstance(),只能测真实单例
  3. 有状态单例线程安全需自理 :单例只保证唯一,ConcurrentHashMap 是你自己选的;若换成 HashMap,并发下就出问题
  4. 反射/序列化可破坏 :普通实现可被反射 setAccessible(true) 造出第二个实例(枚举实现可防)
  5. 多 ClassLoader / 多 JVM 不唯一 :不同类加载器各有一份;多机部署时每台机器各一个,跨机器不共享
  6. 初始化顺序难控 :饿汉式在类加载时创建,若构造器依赖其他尚未初始化的资源,可能出问题

适用场景: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”

优点

  1. 依赖显式化 :构造器注入,从类签名就能看出依赖谁,可读性好
  2. 极易测试 :单测时直接 new OrderService(mockCache),无需任何框架
  3. 生命周期由容器管理 :创建、销毁、初始化回调都由 Spring 统一处理,无手动管理负担
  4. 天然支持 AOP :事务、日志、缓存注解等切面可无缝织入
  5. 可替换、可配置 :面向接口注入,换实现只改配置;可通过 @Scope 灵活改作用域
  6. 无反射破坏风险 :不暴露静态 getInstance(),外部无法绕过容器造实例(容器外 new 的对象不归容器管,但业务代码不会这么做)
  7. 与生态整合 :和 Spring 的事务、安全、监控等基础设施天然协同

缺点

  1. 依赖 Spring 容器 :脱离 Spring 环境(纯工具库、SDK、非 Spring 应用)无法使用
  2. 单例范围是容器级 :多个 ApplicationContext(如多容器测试)会各有一份,不是 JVM 级唯一
  3. 单例 ≠ 线程安全 :容器只保证唯一,Bean 内有可变成员变量时仍需自己保证线程安全(推荐无状态 Bean)
  4. 隐式约定 :默认 singleton 是"约定",新人可能误以为每次注入都是新对象,或误在单例里放请求级状态导致串数据
  5. 容器外 new 不受控 :如果有人在容器外 new LocalCacheService(),会得到第二个实例,容器管不到(但规范开发下不会这么做)

适用场景:绝大多数 Spring 企业级业务开发——这是最主流、最推荐的落地方式。


作者:RCCS_1154
出处:http://www.cnblogs.com/RCCS/
本文版权归作者和博客园共有,转载必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。

posted @ 2026-10-27 11:12  RCCS_1154  阅读(6)  评论(0)    收藏  举报