设计模式 - 单例模式
单例模式1.定义保证一个类仅有一个实例并提供一个访问它的全局访问点。2.补充类是无状态的。Singleton模式用在单线程应用中是否已经实例化由实例自己判断或由它的管理者判断而不是使用者的责任。私有化实例类的构造函数使的使用者不可能违规来创建单例类的实例。3.饿汉式单例类即静态初始化方式它在类加载时即实例化对象要提前占用资源线程安全。4.懒汉式单例类单检查锁定懒汉式单例类在第一次被引用时实例化非线程安全需要做同步处理。但单检查锁定每次调用时都有都锁定开销效率低。双检查锁定Double-Checked Locking模式即第一次检查后再锁定效率高。在检查到实例为null之后进行同步在同步代码中再检查一次实例是否为null确保实例尚未创建。意图是优化掉不必要的锁定这种同步检查最多在实例创建前进行一次因此不会成为瓶颈。双重检查锁定模式不能100%保证实例唯一性加上volatile易变限定符是必要的补充措施有两个作用保证实例引用变量在多个线程中的可见性以防止出现多次创建实例。保证实例引用变量在多个线程中的不可重排序以防止出现使用未初始化的对象。双重检查锁定模式Kotlin实现实现步骤主构造方法私有化通过伴生对象创建实例使用lazy委托属性实现延迟初始化线程安全配置通过LazyThreadSafetyMode.SYNCHRONIZED参数保证线程安全使用静态内部类持有单例在Java中使用静态内部类也称为静态持有者是推荐的一种方式因为它既达到了单例的目的又避免了在单例类被加载时立即实例化单例对象。因为外部类加载时并不会立即加载内部类进而不会立即创建内部类中的实例。只有当第一次显示访问内部类时才会加载内部类。静态内部类类装载方式是更简洁安全的实现无需volatile且天然线程安全。静态内部类方式定义一个静态内部类该内部类持有对外部类的唯一实例。在外部类中提供一个静态方法该方法返回静态内部类中持有并实例化的单例对象。登记式也可以叫做“静态内部类”式。只有调用内部类的时候内部类才开始加载延时加载非静态内部类要依附于外部类必须创建外部类对象才能使用非静态内部类静态内部类不用外部类创建对象就可以使用优点线程安全利用类加载机制延迟加载外部类加载时不会立即加载内部类高效不需要同步锁示例代码// 单例类 public class Singleton { // 私有构造函数 private Singleton() { } // 静态内部类持有静态单例对象 private static class SingletonHolder { private static final Singleton INSTANCE new Singleton(); } // 静态方法返回静态单例对象 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }Kotlin实现实现方式使用伴生对象(companion object)创建私有对象声明SingletonProvider其中声明并初始化单例属性通过lazy委托实现延迟加载特性对比与Java区别Kotlin使用对象声明替代静态内部类线程安全默认使用LazyThreadSafetyMode.SYNCHRONIZED保证线程安全语法简洁通过by lazy简化延迟初始化实现class Singleton4Kotlin private constructor() { private object SingletonProvider { val holder Singleton4Kotlin() } companion object { val instance: Singleton4Kotlin by lazy { SingletonProvider.holder } } }优点线程安全这种方式是线程安全的在多线程环境中也能保证单例的唯一性。延迟加载只有在调用getInstance()方法时才会加载SingletonHolder类从而创建Singleton的实例。这种方式也被称为“双重校验锁”模式的变种但不是双重校验锁的实现方式。简单实现简单易于理解。总结静态内部类方式是实现单例模式的一种非常高效且简洁的方式特别适合在需要确保线程安全同时又希望延迟加载实例的情况下使用。这种方法结合了懒加载和线程安全的优点。为什么需要volatile防止指令重排序在 Java 多线程编程中静态变量若被多个线程共享且涉及对象初始化如单例模式确实建议使用volatile修饰以防止因指令重排序导致未完全初始化的对象被其他线程引用。对象创建过程在 JVM 底层并非原子操作通常分为以下三步分配内存空间初始化对象调用构造方法将对象引用赋值给变量若无volatile修饰JVM 或 CPU 可能为优化性能而将步骤 2 和 3 重排序为1 → 3 → 2即先分配内存立即将引用赋值给变量此时对象尚未初始化再执行构造函数初始化。此时若另一个线程第一次检查变量会发现它 不为 null于是直接返回一个 未初始化的“半成品”对象可能导致空指针异常、数据错误等难以复现的并发 Bug 。volatile如何解决volatile通过以下机制确保安全禁止指令重排序在volatile变量的写操作前后插入 内存屏障Memory Barrier确保初始化操作步骤 2必须在引用赋值步骤 3之前完成 。保证可见性一个线程完成对象初始化并写入volatile变量后其他线程能立即看到最新值不会读到缓存中的null或部分初始化状态 。典型应用场景双重检查锁定DCL单例模式public class Singleton { private static volatile Singleton instance; // 关键volatile 修饰 private Singleton() {} public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // 若无 volatile可能重排序 } } } return instance; } }✅ 加volatile确保instance new Singleton()按 分配 → 初始化 → 赋值 顺序执行避免返回未初始化对象。❌ 不加volatile在高并发下极可能返回半初始化对象引发不可预测错误 。补充说明volatile不保证原子性因此不能替代锁如synchronized或ReentrantLock用于复合操作如count。在单例模式中若追求更简洁安全的实现可考虑 静态内部类 方式无需volatile且天然线程安全 public class Singleton { private Singleton() {} private static class Holder { private static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }总结静态变量是否需用volatile修饰→ 仅在多线程环境下该变量涉及对象引用赋值且存在初始化过程时才需要如 DCL 单例。核心目的禁止指令重排序防止未初始化对象被引用同时保证可见性 。单例模式特点全局唯一在整个程序中只有一个对象。什么样的类适合单例单例可以在其它模式中使用。全局使用的类创建和销毁会消耗很多系统资源的类数据库连接池工厂类数据源应用Spring的Bean默认情况下是单例项目中读取配置文件的类一般也只有一个对象。没有必要每次使用配置文件数据每次new一个对象去读取。应用程序的日志应用一般都何用单例模式实现这一般是由于共享的日志文件一直处于打开状态因为只能有一个实例去操作否则内容不好追加。数据库连接池的设计一般也是采用单例模式因为数据库连接是一种数据库资源。操作系统的文件系统也是大的单例模式实现的具体例子一个操作系统只能有一个文件系统。Application 也是单例的典型应用Servlet编程中会涉及到在servlet编程中每个Servlet也是单例在spring MVC框架/struts1框架中控制器对象也是单例反射和反序列化都可能破坏单例模式的唯一性反射和反序列化都可能破坏单例模式的唯一性导致一个类产生多个实例违背单例设计初衷。反射如何破坏单例单例模式通常通过将构造函数设为private来防止外部直接实例化。但 Java 的反射机制可以在运行时绕过访问控制强制调用私有构造函数从而创建新实例。ConstructorSingleton constructor Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton evilInstance constructor.newInstance(); // 破坏单例反序列化如何破坏单例如果单例类实现了Serializable接口序列化后通过ObjectInputStream.readObject()反序列化时会重新创建一个新对象而非返回原有实例。原因反序列化底层使用反射调用无参构造函数或特殊机制重建对象不经过getInstance() 。测试结果序列化前后的对象引用不同破坏单例。如何防止破坏防止反射破坏在构造函数中添加实例检查private Singleton() { if (instance ! null) { throw new RuntimeException(不允许通过反射创建多个实例); } }注意此方法对懒汉式单例在多线程下可能失效因构造函数可能被多次调用 。防止反序列化破坏在单例类中实现readResolve()方法返回原有实例private Object readResolve() { return getInstance(); }反序列化时JVM 会自动调用该方法返回原单例实例而非新建对象 。更优解使用枚举实现单例枚举是 Java 中最安全的单例实现方式能天然抵御反射和反序列化破坏 。原因枚举类继承自java.lang.Enum没有无参构造器反射调用getDeclaredConstructor()失败。即使强制反射创建JVM 会抛出IllegalArgumentException。反序列化时通过Enum.valueOf()返回已有枚举常量不会新建对象 。示例public enum Singleton { INSTANCE; // 其他业务方法 }✅ 推荐若无特殊限制优先使用枚举实现单例简洁且安全 。总结破坏方式是否可行防护措施推荐方案反射✅ 是构造函数中抛异常枚举最安全反序列化✅ 是实现readResolve()方法枚举自动防护最终建议除非有特殊需求使用枚举实现单例避免手动处理反射和序列化问题核心优势线程安全JVM 在类加载时保证枚举实例唯一创建无需额外同步 。防止反射攻击java.lang.reflect.Constructor对枚举类型显式禁止通过setAccessible(true)创建新实例会抛出IllegalArgumentException。防止序列化破坏枚举的序列化机制确保反序列化时始终返回同一个实例通过valueOf()查找而非新建。代码简洁仅需 1–3 行代码即可完成单例定义无冗余逻辑 。java1.5之后出现 目前推荐实现单例的最佳方式 线程安全 立即初始化 自动支持序列化防止反序列化创建新的对象 防止反射攻击适用场景✅ 全局唯一资源管理如日志、配置、数据库连接池✅ 需防范反射或序列化攻击的系统✅ 不需要延迟初始化枚举为饿汉式加载❌ 若需懒加载可考虑静态内部类方式 ❌ 枚举不能继承其他类但可实现接口ThreadLocal实现线程内单例优点 空间换时间 延时加载缺点 只能是在同一个线程中获得的两个对象才是单例public class Singleton { private Singleton() { } private static ThreadLocalSingleton threadLocalSingleton new ThreadLocalSingleton() { Override protected Singleton initialValue(){ return new Singleton(); } }; public static Singleton getInstance(){ return threadLocalSingleton.get(); } }通过CAS实现单例缺点 可能会产生垃圾对象上述几种单例模式的比较饿汉式线程安全、反射不安全、反序列化不安全、非延时加载。懒汉式线程不安全、反射不安全、反序列化不安全、延时加载。双重检测锁线程安全、反射不安全、反序列化不安全、延时加载。登记式线程安全、反射安全、反序列化不安全、延时加载。枚举式线程安全、反射安全、反序列化安全、非延时加载。ThreadLocal不加锁以空间换时间为每个线程提供独立副本可以保证各自线程是单例的但是不同线程之间不是单例的。CAS无锁乐观策略线程安全。