单例模式是什么
单例保证一个类只有一个实例,并提供全局访问点:进程内共享的配置、连接池、注册表是常见场景。它解决访问点与生命周期的唯一性问题,代价是引入全局状态——依赖变得隐蔽,测试之间容易互相污染。多数情况下,用依赖注入容器持有一个实例更可控。
基本信息
| 意图 | 唯一实例加全局访问点 |
|---|---|
| 实现要点 | 私有构造、惰性初始化、线程安全 |
| 语言差异 | Go 常用一次性初始化原语,Java 常用静态内部类或枚举 |
| 替代方案 | 依赖注入容器注册单实例 |
| 作用域 | 每进程唯一,分布式部署下天然不唯一 |
典型使用场景
- 进程内共享的配置读取器
- 无状态工具类与度量注册表
- 连接池或线程池的持有者
- 日志与指标上报入口
- 需要严格控制并发初始化的稀缺资源
静态内部类实现
静态内部类实现
// 类加载时惰性初始化,由虚拟机保证线程安全
class Config {
private Config() {} // 私有构造,禁止外部创建
private static class Holder {
static final Config INSTANCE = new Config();
}
static Config instance() { return Holder.INSTANCE; }
}几种实现的差异
| 实现方式 | 线程安全 | 说明 |
|---|---|---|
| 静态内部类 | 由类加载机制保证 | 惰性且无需显式加锁 |
| 枚举 | 语言层面保证 | 序列化与反射场景也安全 |
| 双重检查锁 | 需正确使用 volatile | 漏写修饰符易出可见性问题 |
| 容器单例 | 由容器管理 | 依赖可注入、可替换 |
单例的唯一作用域要写清:是每进程唯一还是每容器唯一。集群部署时它是多个实例,涉及共享状态就得另找方案。
把全局可变状态塞进单例到处读写,依赖就从签名里消失了:测试互相污染、并发问题难以复现,想替换实现时也无处下手。
测试需要隔离单例状态时,可以在启动阶段注入替身或提供重置入口;如果做不到,说明访问方式绑得太紧,先把依赖改成注入会更容易测试。