创建型:单例模式

发布于:2025-05-19 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

1、核心思想

2、实现方式

2.1 饿汉式

2.2 懒汉式

2.3 枚举(Enum)

3、关键注意事项

3.1 线程安全

3.2 反射攻击

3.3 序列化与反序列化

3.4 克隆保护

4、适用场景


1、核心思想

目的:确保一个类仅有一个实例

功能:供全局访问点(通过静态方法或变量提供全局访问入口),可以选择延迟加载。

优点 缺点
严格控制实例数量,节省资源 可能隐藏类之间的依赖关系,降低可测试性
全局访问点方便管理共享资源 违背单一职责原则(管理自身生命周期)
避免频繁创建销毁对象 多线程环境需额外处理同步问题

2、实现方式

2.1 饿汉式

饿汉式:即在初始阶段就主动进行实例化,并时刻保持一种渴求的状态,无论此单例是否有人使用。

特点:类加载时立即创建实例,线程安全但可能浪费资源。

public class EagerSingleton {
    // static:在类加载时初始化,与类同在;  final:一旦被赋值不能被更改
    private static final EagerSingleton instance = new EagerSingleton();
    
    // 私有构造函数,禁止外部调用创建对象
    private EagerSingleton() {} 
    
    public static EagerSingleton getInstance() {
        return instance;
    }
}

2.2 懒汉式

懒汉式:在第一次使用时,再进行初始化,防止没有人调用,提前初始化造成的资源浪费。

特点:延迟实例化,需处理线程安全问题。

1> 同步锁版本(不推荐)

public class SynchronizedSingleton {
   
    private static SynchronizedSingleton instance;
    
    private SynchronizedSingleton() {}
    
    public static synchronized SynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

注意: 变量不能使用final,会导致被初始化为null,之后不可赋值

缺点:线程还没进入方法内,就先加锁排队,造成线程阻塞,资源和时间的浪费。

2> 双重检查锁(Double-Checked Locking)

public class DCLSingleton {
     // volatile:防止指令重排序,保证变量值在各线程访问时的同步性、唯一性
    private static volatile DCLSingleton instance;
    
    private DCLSingleton() {}
    
    public static DCLSingleton getInstance() {
        if (instance == null) { // 第一次检查,放宽入口,保证线程并发高效性
            synchronized (DCLSingleton.class) {
                if (instance == null) { // 第二次检查,加锁同步,保证实例化单次运行
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

相比“懒汉模式”​,其实在大多数情况下我们通常会更多地使用“饿汉模式”​,原因在于这个单例迟早是要被实例化占用内存的,延迟懒加载的意义并不大,加锁解锁反而是一种资源浪费,同步更是会降低CPU的利用率,使用不当的话反而会带来不必要的风险。

2.3 枚举(Enum)

特点:天然防止反射和序列化攻击,线程安全(推荐方式)

public enum EnumSingleton {
    INSTANCE; // 这是一个单例对象
    
    public void doSomething() {
        // 业务方法
    }
}


3、关键注意事项

3.1 线程安全

多线程环境下需确保实例唯一性(如双重检查锁、枚举等)。

3.2 反射攻击

通过反射可绕过私有构造函数,需额外防护(如枚举或抛出异常)。

攻击示例:

// 反射攻击代码:
Class<?> clazz = Singleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true); // 绕过 private 权限
Singleton hackedInstance = (Singleton) constructor.newInstance(); // 创建新实例!

防御方法:在构造方法中抛异常(检测是否已存在实例,若存在则抛出异常

private Singleton() {
    if (INSTANCE != null) {
        throw new IllegalStateException("单例已被创建,禁止反射调用!");
    }
}

3.3 序列化与反序列化

反序列化可能创建新实例,需实现 readResolve() 方法。

攻击示例:

// 序列化攻击代码:
Singleton instance1 = Singleton.getInstance();
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(instance1);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Singleton instance2 = (Singleton) ois.readObject(); // 新实例!

防御方法:实现 readResolve() 方法,直接返回已有实例,覆盖反序列化逻辑。

public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    // 反序列化时直接返回 INSTANCE
    protected Object readResolve() {
        return INSTANCE;
    }
}

3.4 克隆保护

重写 clone() 方法并抛出异常。

若单例类实现了 Cloneable 接口,并直接使用默认的 clone() 方法(或自定义实现未做防御),攻击者可以通过调用 clone() 创建新实例,破坏单例的唯一性。

如果类不实现 Cloneable 接口,调用 clone() 会抛出 CloneNotSupportedException。

即使类未实现 Cloneable,也可显式重写 clone() 方法禁止克隆:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }

    // 防御 clone 攻击
    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("禁止克隆单例!");
    }
}

4、适用场景

  • 资源共享:如数据库连接池、线程池。

  • 配置管理:全局配置类避免重复加载。

  • 日志记录:统一日志写入入口。

  • 设备驱动:如打印机唯一控制实例。


网站公告

今日签到

点亮在社区的每一天
去签到