单例模式(Singleton):一个类只能有一个实例
单例模式,说白了就是——一个类在整个系统里只能有一个对象。你想想看,有些东西多了反而麻烦,比如配置管理器、线程池、日志对象。如果每个地方都 new 一个,那资源就乱套了。
我个人习惯把单例模式看作是「全局唯一入口」。它保证了两件事:一是实例唯一,二是全局可访问。嗯,听起来简单,但实现起来有不少坑。我踩过不少,今天跟你聊聊。
为什么需要单例?
我在项目中遇到过这样一个场景:一个日志系统,如果每个模块都自己 new 一个 Logger,那日志文件会被多个实例同时写,内容乱成一锅粥。单例模式就能解决这个问题——所有代码都通过同一个实例写日志,顺序可控,性能也高。
常见的应用场景还有:
- 数据库连接池
- 配置信息读取器
- 线程池管理器
- 缓存系统
四种实现方式,各有千秋
单例模式的实现方式有好几种,我按「从简单到复杂」的顺序给你捋一遍。每种都有它的适用场景,别盲目选。
1. 饿汉式(Eager Initialization)
最简单粗暴的方式。类加载时就创建实例,不管你用不用。
public class SingletonEager {
private static final SingletonEager instance = new SingletonEager();
private SingletonEager() {}
public static SingletonEager getInstance() {
return instance;
}
}
优点:线程安全,实现简单。
缺点:如果这个类很重,但一直没被使用,就白白占着内存。
2. 懒汉式(Lazy Initialization)
等到第一次调用 getInstance() 时才创建实例。节省资源,但要注意线程安全。
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy() {}
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
这里加了 synchronized 关键字,保证多线程环境下不会创建多个实例。但问题也很明显——每次调用 getInstance() 都要加锁,性能损耗大。
3. 双重检查锁(Double-Checked Locking)
这个方案既保证了懒加载,又减少了锁的开销。只有第一次创建实例时才加锁,后续调用直接返回。
public class SingletonDCL {
private static volatile SingletonDCL instance;
private SingletonDCL() {}
public static SingletonDCL getInstance() {
if (instance == null) {
synchronized (SingletonDCL.class) {
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
}
这里的关键是 volatile 关键字。为什么需要它?因为 instance = new SingletonDCL() 不是原子操作,JVM 可能会重排序,导致其他线程拿到一个「半初始化」的对象。volatile 禁止了指令重排,保证安全。
4. 静态内部类(Static Inner Class)
这是我个人最喜欢的方式。它利用了 JVM 的类加载机制,既实现了懒加载,又天然线程安全,还没有锁的开销。
public class SingletonHolder {
private SingletonHolder() {}
private static class Holder {
private static final SingletonHolder INSTANCE = new SingletonHolder();
}
public static SingletonHolder getInstance() {
return Holder.INSTANCE;
}
}
原理很简单:外部类加载时,内部类 Holder 并不会被加载。只有调用 getInstance() 时,JVM 才会加载 Holder,并创建实例。而 JVM 保证类加载过程是线程安全的。
四种方式对比
| 实现方式 | 懒加载 | 线程安全 | 性能 | 代码复杂度 |
|---|---|---|---|---|
| 饿汉式 | 否 | 是 | 高 | 低 |
| 懒汉式 | 是 | 是(加锁) | 低 | 低 |
| 双重检查锁 | 是 | 是 | 高 | 中 |
| 静态内部类 | 是 | 是 | 高 | 低 |
避坑指南
我曾经在面试中问过很多人:「单例模式有什么坑?」大部分人答不上来。这里我列几个常见的:
- 反射破坏单例:通过反射调用私有构造器,可以创建多个实例。解决方案是在构造器里加一个标志位,第二次调用时抛异常。
- 序列化破坏单例:如果单例类实现了 Serializable,反序列化时会创建新对象。需要重写
readResolve()方法。 - 多类加载器环境:不同类加载器会加载出不同的单例实例。这个比较少见,但如果你在 Web 容器里部署多个应用,要注意。
知识体系图
下面这张图帮你理清单例模式的核心脉络:
总结
单例模式看起来简单,但实现细节里藏着不少门道。我个人建议:
- 如果项目不大,用饿汉式最省事
- 如果追求性能和懒加载,用静态内部类
- 双重检查锁也行,但记得加 volatile
- 懒汉式加锁的方式,能不用就别用了
嗯,单例模式就聊到这儿。记住一句话:唯一,但不滥用。