Administrator
Administrator
发布于 2026-01-18 / 4 阅读
0
0

[设计模式] 单例模式

设计模式

单例模式

方案 延迟加载 (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 层面不是原子操作,它分三步:

  1. 给对象分配内存空间。
  2. 初始化对象(执行构造函数)。
  3. 将 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...");
    }
}

评论