深入netty18-netty中的设计模式-单例模式详解

单例模式实现全解

实现1:静态方法

public final class Singleton implements Serializable {
    private Singleton() {}
    private static final Singleton INSTANCE = new Singleton();
    public static Singleton getInstance() {
        return INSTANCE;
    }
    public Object readResolve() {
        return INSTANCE;
    }
}

问题1:为什么加 final

  • final 关键字用于声明 Singleton 类不能被继承。这样可以防止其他类继承 Singleton 类并可能破坏单例模式。

问题2:如果实现了序列化接口,还要做什么来防止反序列化破坏单例

  • 即使 Singleton 类实现了 Serializable 接口,仅仅这样还不足以防止反序列化破坏单例。在反序列化过程中,如果没有额外的处理,可能会创建一个新的 Singleton 实例。

  • 为了解决这个问题,Singleton 类提供了 readResolve() 方法。当反序列化 Singleton 对象时,readResolve() 方法会被调用,它返回单例实例的引用,确保反序列化不会创建新的实例。

问题3:为什么设置为私有?是否能防止反射创建新的实例?

  • 构造函数被设置为私有是为了防止外部通过 new 关键字直接创建 Singleton 类的新实例。

  • 然而,私有构造函数并不能完全防止反射攻击。通过反射,可以访问私有构造函数并使用它来创建新的实例。为了防止这种情况,通常需要在构造函数中添加安全检查,或者使用其他机制(如枚举)来实现单例。

问题4:这样初始化是否能保证单例对象创建时的线程安全?

  • INSTANCE 变量被声明为 private static final,并且是通过直接赋值的方式初始化的。在Java中,对于 final 字段,如果它是一个不可变对象(如 String、基本数据类型包装类或枚举),那么它会被初始化为一个常量表达式,并且在类加载时就已经完成初始化。

  • 因此,INSTANCE 的初始化是线程安全的,它在类加载时就已经被创建,且不可变。这确保了单例对象的创建是线程安全的。

问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public

  • INSTANCE 设置为 public 会允许外部直接访问单例实例,这可能会导致单例模式被破坏,例如外部可以修改单例实例的状态。

  • 通过提供一个公共的静态方法 getInstance() 来访问单例实例,可以在访问时添加额外的逻辑,例如安全检查或延迟初始化。

  • 使用静态方法还可以在未来更改单例的实现,而不影响使用单例的客户端代码,因为客户端代码依赖的是方法签名,而不是具体的字段。

实现2:枚举

enum Singleton { 
    INSTANCE; 
}

使用枚举实现单例是一种简洁且线程安全的方式。以下是对您提出问题的回答:

问题1:枚举单例是如何限制实例个数的

  • 在Java中,枚举类型本身是单例的。每个枚举常量在Java虚拟机(JVM)中只会有一个实例。

问题2:枚举单例在创建时是否有并发问题

  • 枚举实例在JVM加载枚举类时创建,因此它们是线程安全的。即使多个线程尝试同时访问枚举实例,JVM也会保证只有一个实例被创建。

问题3:枚举单例能否被反射破坏单例

  • 由于枚举常量是静态且不可变的,通过反射创建新实例是不可能的。Java语言规范确保了枚举常量的唯一性。

问题4:枚举单例能否被反序列化破坏单例

  • 枚举类型的反序列化过程由JVM控制,它保证了反序列化后的枚举实例与枚举类中定义的实例相同,不会破坏单例模式。

问题5:枚举单例属于懒汉式还是饿汉式

  • 枚举单例实际上不属于懒汉式或饿汉式。它们在类加载时就实例化,类似于饿汉式,但由于枚举的特殊性,它们在任何情况下都保证只有一个实例。

问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做

  • 可以在枚举常量中定义方法来执行初始化逻辑。由于枚举常量在JVM加载枚举类时就创建,你可以在枚举常量中放置初始化代码或调用一个初始化方法。

public enum Singleton {
    INSTANCE;
    
    // 初始化逻辑可以在这里定义
    private Resource resource;
    
    // 枚举实例的构造函数
    Singleton() {
        resource = new Resource();
        // 其他初始化逻辑
    }
    
    public Resource getResource() {
        return resource;
    }
    
    // 其他方法
}

在这个例子中,Singleton 枚举类型有一个名为 INSTANCE 的实例,它在类加载时创建。构造函数 Singleton() 中包含了初始化逻辑。通过 getResource() 方法可以访问初始化后的资源。

枚举单例是一种非常强大和推荐的方式来实现单例模式,因为它提供了语言层面的保证,确保了单例的唯一性和线程安全性。

实现3:单重检查锁

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    public static synchronized Singleton getInstance() {
        if( INSTANCE != null ){
            return INSTANCE;
        } 
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

实现4:双重检查锁

public final class Singleton {
    private Singleton() { }
    
    // 问题1:解释为什么要加 volatile ?
    private static volatile Singleton INSTANCE = null;
    
    // 问题2:对比实现3, 说出这样做的意义 
    public static Singleton getInstance() {
        if (INSTANCE != null) { 
            return INSTANCE;
        }
        synchronized (Singleton.class) { 
            // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
            if (INSTANCE != null) { // t2 
                return INSTANCE;
            }
            INSTANCE = new Singleton(); 
            return INSTANCE;
        } 
    }
}

问题1:为什么要加 volatile

在Java中,volatile 关键字用于确保变量的读写操作对所有线程都是可见的,并且每次读取都能获取到最新的值。在多线程环境中,如果不使用 volatile,可能会出现指令重排的问题,导致其他线程看不到单例实例已经被初始化。

具体来说,在没有 volatile 的情况下,创建单例对象 Singleton 的过程涉及几个步骤:

  1. 分配内存空间。

  2. 初始化对象。

  3. 设置 INSTANCE 引用指向分配的内存地址。

由于Java内存模型允许指令重排,上述步骤可能会被重排到第三步之后执行,这意味着在 INSTANCE 引用被设置之前,其他线程可能会看到部分初始化的单例实例,这将导致错误的状态被其他线程观察到。

使用 volatile 关键字可以防止这种指令重排,确保在 INSTANCE 引用被设置之前,对象的初始化工作已经完成。

问题2:为什么要双重检查锁?

双重检查锁定是一种减少获取锁开销的技术。在第一次检查 INSTANCE != null 时,如果没有单例实例,线程将获取锁。在获取锁之后,进行第二次检查 INSTANCE != null 来确保在此期间没有其他线程已经创建了实例。这样可以减少在单例实例已经创建后,其他线程获取锁的频率,从而提高性能。

问题3:为什么还要在这里加为空判断,之前不是判断过了吗?

双重检查锁定中的第二次检查是必要的,因为即使第一次检查确认 INSTANCEnull,也可能有其他线程在当前线程进入同步块之前已经创建了单例实例。

考虑以下场景:

  1. 线程 T1 发现 INSTANCEnull

  2. 线程 T1 准备获取锁,但此时线程调度器决定暂时挂起 T1。

  3. 线程 T2 恢复并发现 INSTANCEnull,然后获取锁并创建了单例实例。

  4. 线程 T2 释放锁。

  5. 线程 T1 恢复,进入同步块,并再次检查 INSTANCE,此时它仍然是 null,因此 T1 会创建一个新的实例,这是错误的。

通过在同步块内进行第二次检查,可以确保即使在上述情况下,也不会创建多个实例。

(推荐的)实现5:内部类->类加载时初始化

public final class Singleton {
    private Singleton() { }
    // 问题1:属于懒汉式还是饿汉式  懒汉式
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    // 问题2:在创建时是否有并发问题   JVM保证其安全性
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

问题1:属于懒汉式还是饿汉式?

这个实现属于懒汉式。在懒汉式单例模式中,实例是在第一次调用 getInstance() 方法时创建的,而不是在类加载时就创建。这种方式的优点是可以推迟对象的创建直到真正需要它的时候,从而减少内存占用。缺点是必须适当地同步 getInstance() 方法以确保线程安全,或者使用其他机制来保证只创建一个实例。

问题2:在创建时是否有并发问题?

在这个特定的实现中,没有并发问题。原因是 Java 保证了一个类的加载过程是线程安全的。当一个类第一次被主动使用时,JVM 会同步地加载这个类。由于 LazyHolderSingleton 类的内部静态类,它只有在第一次调用 getInstance() 方法时才会被加载。此时,JVM 会保证只有一个线程可以创建 LazyHolder 类的实例。

此外,由于 INSTANCELazyHolder 类的静态成员,并且是在静态初始化块中初始化的,Java 也保证了静态初始化块的执行是线程安全的。这意味着 LazyHolder.INSTANCE 的创建对所有线程来说是原子的,并且只执行一次。

这种实现方式通常被称为“比尔·波恩(Bill Pugh)的单例模式”,它结合了懒汉式和饿汉式单例模式的优点:它既实现了延迟加载,又避免了同步带来的性能开销,并且是线程安全的。

总结来说,这个 Singleton 类实现了一个线程安全的懒汉式单例模式,且由于 JVM 对类加载过程的内部细节保证,创建实例时不会有并发问题。

为什么更推荐实现5:LazyHolder 方式

推荐使用 LazyHolder 方式(也称为比尔·波格单例模式)而不是双重检查锁定(DCL)的主要原因在于线程安全性和简洁性。

LazyHolder 方式相对于双重检查锁定的一些优势:

  1. 线程安全性

    • 比尔·波格单例模式:利用了 Java 的类加载机制来确保线程安全。静态内部类 LazyHolder 只有在第一次使用时才会加载,Java 保证了一个类的加载过程是线程安全的,因此不需要额外的同步措施。

    • 双重检查锁定:在多线程环境中,双重检查锁定可能会遇到可见性和指令重排的问题,导致不正确的行为。虽然在 Java 5 及以后的版本中,volatile 关键字的使用可以保证可见性,但双重检查锁定的正确实现仍然相对复杂。

  2. 简洁性

    • 比尔·波格单例模式:提供了一种更简洁的方式来实现单例,代码更易于理解和维护。

    • 双重检查锁定:需要使用 volatile 关键字,并且要正确管理同步块,这增加了代码的复杂性。

  3. 性能

    • 比尔·波格单例模式:不需要在每次获取实例时进行同步,减少了性能开销。

    • 双重检查锁定:虽然减少了同步的使用,但仍然需要在 getInstance() 方法中进行两次检查,可能会有轻微的性能影响。

  4. 避免复杂的锁定机制

    • 比尔·波格单例模式:避免了复杂的锁定机制,只依赖于 Java 的类加载机制。

    • 双重检查锁定:需要手动管理锁,增加了出错的可能性。

  5. 避免反射和序列化问题

    • 比尔·波格单例模式:更难通过反射攻击破坏单例模式,因为单例实例是通过私有的静态内部类创建的。

    • 双重检查锁定:可能更容易受到反射攻击,因为单例实例的创建依赖于公共的构造函数。

  6. 易于扩展

    • 比尔·波格单例模式:如果需要修改单例的创建逻辑,只需修改 LazyHolder 类即可。

    • 双重检查锁定:任何修改都可能需要重新审视同步机制,以确保线程安全。

netty中单例模式

Netty 中使用饿汉式单例模式的实践很常见,因为这种模式简单且线程安全,适合用于创建全局的、只被实例化一次的对象。以下是一些 Netty 中使用饿汉式单例模式的源码示例:

  1. 选择策略对象DefaultSelectStrategy

    final class DefaultSelectStrategy implements SelectStrategy {
        static final SelectStrategy INSTANCE = new DefaultSelectStrategy();
        private DefaultSelectStrategy() { }
        @Override
        public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
            return hasTasks ? selectSupplier.getAsInt() : SelectStrategy.SELECT;
        }
    }
  2. MQTT消息编码器MqttEncoder

    public class MqttEncoder {
        public static final MqttEncoder INSTANCE = new MqttEncoder();
        private MqttEncoder() { }
        // Encoder methods...
    }
  3. 读超时异常类ReadTimeoutException

    public final class ReadTimeoutException extends IOException {
        public static final ReadTimeoutException INSTANCE = new ReadTimeoutException();
        private ReadTimeoutException() {
            super("read timed out");
        }
    }

    此外还有WriteTimeoutExceptionClosedChannelException等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值