设计模式
单例模式
| 方案 | 延迟加载 (Lazy) | 线程安全 | 防反射/序列化 | 推荐场景 |
|---|---|---|---|---|
| 饿汉式 | 否 | 是 | 否 | 对象较小、一定会用到时 |
| 双重检查锁 (DCL) | 是 | 是 | 否 | 对内存敏感、通用的业务场景 |
| 静态内部类 | 是 | 是 | 否 | 常规开发首选,代码最优雅 |
| 枚举 | 否 (类加载即初始化) | 是 | 是 | 对安全性要求极高、系统底层组件 |
饿汉式:类加载时就创建对象
public class HungrySingleton {
// 饿汉式,简单写,但是费内存(不管用不用直接加载)
private static final HungrySingleton singleton;
static {
singleton = new HungrySingleton();
System.out.println("饿汉式创建");
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return singleton;
}
}
懒汉式: 用到时再创建
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
// 这种存在隐患,并发时可能创建多个,需要用synchronized修饰
// 并且这种每次获取都要上锁,性能有问题
public synchronized static LazySingleton getInstance() {
if (instance == null) {
System.out.println("懒汉式创建");
instance = new LazySingleton();
}
return instance;
}
}
静态内部类
public class HolderSingleton {
private HolderSingleton() {
}
// 静态内部类:只有在被调用时才会加载
private static class SingletonHolder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
// 只有这里显式调用了 SingletonHolder,JVM 才会去初始化它
return SingletonHolder.INSTANCE;
}
}
双重检查锁
为什么要检查两次? 第一次检查:是为了性能。如果对象已经创建好了,就不要再去抢锁了,直接拿走,大大减少了锁的开销。 第二次检查:是为了安全。假设线程 A 和 B 同时通过了第一次检查,A 抢到了锁,创建了对象释放锁; 此时 B 拿到锁,如果没有第二次检查,B 会再创建一个对象,单例就被破坏了。
为什么要加 volatile?(必考!) 问题来源:instance = new DclSingleton() 这行代码在 JVM 层面不是原子操作,它分三步:
- 给对象分配内存空间。
- 初始化对象(执行构造函数)。
- 将 instance 引用指向分配的内存地址(执行完这步 instance 就不为 null 了)。指令重排序:CPU 或编译器为了优化,可能会把顺序打乱成 1 -> 3 -> 2。 后果:如果线程 A 执行了 1 和 3(此时对象还没初始化,但引用已经不为 null),线程 B 刚好来执行第一次检查,发现不为 null,直接把这个半成品(未初始化的空壳对象)拿去用了,程序直接报错。 解决:volatile 禁止指令重排序,保证必须按 1 -> 2 -> 3 执行。
public class DoubleCheckedLockSingleton {
// volatile
private static volatile DoubleCheckedLockSingleton instance;
private DoubleCheckedLockSingleton() {}
public static DoubleCheckedLockSingleton getInstance() {
// 第一次检查:如果不是null,则直接返回,避免不必要的锁竞争
if (instance == null) {
synchronized (DoubleCheckedLockSingleton.class) {
// 第二次检查:进入锁之后再确认,防止两个线程同时通过了第一次检查。并且另一个已经创建好后释放锁
if (instance == null) {
instance = new DoubleCheckedLockSingleton();
}
}
}
return instance;
}
}
枚举
为什么防反射?前面的 DCL 和静态内部类,如果有人手贱用反射 Constructor.setAccessible(true),依然可以强行调用私有构造器创建新对象。但在 JDK 源码中(Constructor 类的 newInstance 方法),明确规定了:如果是枚举类型,禁止反射创建,直接抛异常。这是编译器级别的防御。
为什么防序列化破坏?普通单例对象被序列化(写到文件)再反序列化(读回来)时,会创建一个新的对象,破坏单例。Java 对枚举的序列化做了特殊处理:它只传输枚举的名字,反序列化时通过名字去查找对象,而不是创建新对象,从而保证了绝对的单例。
public enum EnumSingleton {
INSTANCE; // 这就是单例对象,且全局唯一
// 可以定义自己的业务方法
public void doSomething() {
System.out.println("Doing business logic...");
}
}