单例模式的两种最佳实践

参考文章https://mp.weixin.qq.com/s/dW0L-PoBeTFHhD29HJO0BQ

单例模式(lazy)

本文仅仅记录一下单例模式的两种最佳实践,想了解详细的推导过程请到这里敖丙单例模式
两种最佳实践:

  • 静态内部类
  • 枚举

·····························仅供参考···································

静态内部类

为什么使用静态内部类

由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

代码实现

public class Lazy {

    /* 私有构造方法,防止被实例化 */
    private Lazy() {
    }

    /* 此处使用一个内部类来维护单例 */
    private static class SingletonFactory {
        private static Lazy instance = new Lazy();
    }

    /* 获取实例 */
    public static Lazy getInstance() {
        return SingletonFactory.instance;
    }

    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
    public Object readResolve() {
        return getInstance();
    }
}
  • 在上述代码中,第一次调用getInstance时,JVM保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕。

  • 静态内部类InnerClass在第一次被触及时,才会被加载和初始化。在首次使用getInstance()方法前,InnerClass根本就不会被加载和初始化。

  • 同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式。

为什么要重写readResolve

当一个单例类被反序列化时,JVM会通过反射调用这个 readResolve() 方法,并将这个方法的返回值作为最终的反序列化对象。
通过重写readResolve() 方法,返回了单例对象 getInstance()。这样,无论序列化多少次,反序列化得到的对象都是同一个单例实例,从而保证了单例模式的约束。

枚举

枚举类实现单例模式是极力推荐的单例实现模式

  • JVM保证了枚举类型是线程安全的,并且只会装载一次
  • 枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式
public enum Singleton {
    /**
     * 定义一个枚举的元素,它就代表了Singleton的一个实例。
     */
    Instance;
}
public enum SingletonEnum {
   // INSTANCE可以调用属性和方法
    INSTANCE;

    private String value;

    SingletonEnum() {
        // 一些初始化代码
        value = "Hello World!"; 
    }

    public String getValue() {
        return value;
    }
}

枚举单例为什么不需要重写 readResolve

在序列化和反序列化过程中,JVM 会保证每个枚举实例只有一个实例。具体来说:

  1. 序列化时,JVM 会写入枚举实例的 name 字符串,而不是整个对象实例。

  2. 反序列化时,JVM 会使用 Enum.valueOf 方法查找相应的枚举实例,而不是重新创建一个新的实例。

枚举类型的序列化

在 Java 中,当序列化一个枚举类型的对象时,JVM 会采用一种特殊的序列化机制,而不是简单地将整个对象的状态写入字节流中:

  1. 获取枚举实例的 name 字符串,例如 SingletonEnum.INSTANCE 的 name 就是 “INSTANCE”。
  2. 将这个 name 字符串写入序列化字节流中。
  3. 反序列化时,JVM 会使用 Enum.valueOf 方法查找相应的枚举实例,而不是重新创建一个新的实例。

换句话说,JVM 并不会将枚举实例对象中所有的字段数据都序列化,而只是序列化了这个枚举实例的名字。
这是因为枚举类型对象的实例在 JVM 中是唯一的,JVM 可以根据枚举实例的名字找到对应的枚举对象。所以,当序列化时只需要写入枚举实例的名字,反序列化时就可以从名字找回唯一的枚举实例对象。

枚举和静态内部类各自的优势

枚举单例模式的优势
  1. 线程安全。枚举类天生就是线程安全的,因为它是由 JVM 从根本上进行保证。
  2. 防止反射攻击。由于枚举类型的构造方法是被 private 修饰的,并且枚举值是 final 类型,所以反射攻击会失败。
  3. 防止序列化攻击。枚举类型的序列化和反序列化由 JVM 特殊处理,可以自动保证单例实例的唯一性。
  4. 代码简洁。枚举单例模式实现起来非常简洁,可读性好。
  5. 天生自带自身类型安全检查。枚举类型在编译时会对枚举值进行类型安全检查,避免了传入非法枚举值。

静态内部类单例模式的优势:

  1. 延迟加载。通过静态内部类的方式,可以实现单例的延迟加载,即只有在第一次使用该单例时才会加载该单例。
  2. 代码可读性较好。代码比较简洁,利于阅读和维护。
  3. 没有更好的方式。枚举方式虽然有优势,但如果单例类还需要序列化且携带数据的话,枚举方式就无法实现了。

总的来说,如果单例类没有太多数据需要随对象一起序列化的话,那么使用枚举实现单例模式无疑是更好的选择。但如果单例类确实需要序列化且携带一些数据,那么静态内部类的方式也是个不错的选择。不过需要注意的是,静态内部类单例模式需要显式地重写 readResolve 方法来防止反序列化时出现多个实例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值