1.3 单例模式
单例模式(Singleton)保证一个类全局只有一个实例,并提供一个统一的访问入口。配置中心、日志器、连接池、缓存,常常被设计成单例。
它的实现看起来很简单,却藏着设计模式里最多的“坑”:懒加载、线程安全、序列化、反射攻击,以及最根本的争议——单例本质上是全局状态。
下面的共享设置实验只看一个现象:只改登录页颜色,其它页面会不会跟着变。会一起变,说明它们拿到的是同一个实例;只有登录页变,说明每个页面各有一个实例。
正在加载交互实验...
三种常见写法
java
// 1) 饿汉式:类加载时就创建,简单且线程安全,但无法懒加载
class Eager {
private static final Eager INSTANCE = new Eager();
private Eager() {}
public static Eager getInstance() { return INSTANCE; }
}
// 2) 静态内部类(holder 习语):懒加载 + 线程安全,推荐
class Lazy {
private Lazy() {}
private static class Holder { static final Lazy INSTANCE = new Lazy(); }
public static Lazy getInstance() { return Holder.INSTANCE; }
}
// 3) 枚举:最简洁,天然防反射与序列化攻击(《Effective Java》推荐)
enum Config { INSTANCE; }注意
新手最爱写“双重检查锁(double-checked locking)”,但很容易忘记
volatile 而踩到指令重排的坑。在 JVM 上,holder 习语或枚举几乎总是更好的选择。正在加载概念检查...
为什么“没加锁的懒汉式”会出错
经典 bug 出在“检查—再创建”这两步不是原子的。两个线程可能:
1. 线程 A 判断 instance == null 为真。
2. 线程 B 也判断 instance == null 为真(A 还没来得及赋值)。
3. 两个线程各自 new 了一个对象——单例被打破,出现了两个实例。
下面的双线程竞态调度器让你亲手交替推进两个线程,观察“无锁”时如何同时推进到两个本地对象,再打开 synchronized 锁看临界区如何把竞态挡住。
正在加载交互实验...
真正的争议:单例是不是反模式
很多资深工程师对单例持保留态度,原因不在于“唯一性”本身,而在于它通常被当作全局变量来用:
- 依赖被隐藏。任何代码都能
Config.getInstance(),你从函数签名里看不出它依赖了什么。
- 可测试性变差。单例的状态跨测试用例残留,难以替换成 mock。
- 并发与生命周期复杂。谁来负责销毁?多线程下的状态竞争怎么办?
更现代的做法是:保留“只有一个实例”这个约束,但通过依赖注入把它传进来,而不是让代码到处主动 getInstance()。这样既保证唯一性,又让依赖显式、可替换。
正在加载概念检查...
正在加载本节练习...