今天介绍一下 单例模式(Singleton)
应用场景:配置管理类、数据库连接池、线程池
实现方式:双重检查锁定、静态内部类、枚举
public class ConfigManager {
private static volatile ConfigManager instance;
private ConfigManager() {}
public static ConfigManager getInstance() {
if (instance == null) {
synchronized (ConfigManager.class) {
if (instance == null) {
instance = new ConfigManager();
}
}
}
return instance;
}
}
在双重检查锁定(Double-Checked Locking)的单例模式实现中,如果缺少volatile
关键字,可能会导致线程安全问题,具体表现为可能获取到未完全初始化的单例对象。这是由Java内存模型(JMM)的特性决定的。
没有volatile
时的问题
指令重排序问题:
- 对象初始化
instance = new ConfigManager()
不是一个原子操作,它包含三个步骤:- 分配内存空间
- 初始化对象
- 将引用指向内存地址
- 编译器/处理器可能会进行指令重排序,导致步骤2和步骤3顺序颠倒
- 这样其他线程可能看到一个不为null但未完全初始化的对象
- 对象初始化
可见性问题:
- 没有volatile修饰,一个线程对instance的修改可能不会立即对其他线程可见
- 可能导致多个线程都认为instance为null,进而创建多个实例
具体场景分析
假设没有volatile:
// 线程A第一次调用getInstance()
if (instance == null) { // 第一次检查
synchronized (ConfigManager.class) {
if (instance == null) { // 第二次检查
instance = new ConfigManager(); // 可能发生重排序
}
}
}
// 线程B此时调用getInstance()
// 可能看到instance不为null,但对象尚未完全初始化
为什么volatile能解决这个问题
禁止指令重排序:
- volatile通过内存屏障(Memory Barrier)禁止JVM和处理器对相关指令进行重排序
- 确保对象的初始化在引用赋值之前完成
保证可见性:
- 对volatile变量的写操作会立即刷新到主内存
- 对volatile变量的读操作会从主内存读取最新值
其他解决方案
静态内部类方式(推荐):
public class ConfigManager { private ConfigManager() {} private static class Holder { static final ConfigManager INSTANCE = new ConfigManager(); } public static ConfigManager getInstance() { return Holder.INSTANCE; } }
- 利用类加载机制保证线程安全
- 延迟加载(Lazy Initialization)
枚举方式(最安全):
public enum ConfigManager { INSTANCE; // 其他方法 }
- 防止反射攻击
- 自动处理序列化问题
在实际项目中,静态内部类方式是最常用的单例实现方式,既保证了线程安全又实现了延迟加载。