单例模式(Singleton)

单例模式(Singleton)

懒汉模式

image-20240914232924512

确保只有一个实例,并且提供实例的全局访问点

懒汉模式

1、懒汉模式-线程不安全

public class LazySingleton_00 {

    private static LazySingleton_00 lazySingleton00;

    // 构造方法私有化
    private LazySingleton_00() {

    }
    public static LazySingleton_00 getLazySingleton() {
        if (Objects.isNull(lazySingleton00)) {
            return lazySingleton00;
        }
        lazySingleton00 = new LazySingleton_00(); // 非线程安全
        return lazySingleton00;
    }
}

2、懒汉模式-线程安全

public class LazySingleton_01 {

    private static LazySingleton_01 lazySingleton01;

    // 构造方法私有话
    private LazySingleton_01() {

    }

    public static synchronized LazySingleton_01 getLazySingleton() {//临界区
        if (Objects.isNull(lazySingleton01)) {
            return lazySingleton01;
        }
        lazySingleton01 = new LazySingleton_01();
        return lazySingleton01;
    }
    
}

从案例1,可以发现创建对象都是通过共有的一个方法获取对象的。当在多线程的场景下,就会出现线程安全问题。想想如果有多个线程进入if (Objects.isNull(lazySingleton01)) ,并且此时lazySingleton00为null ,那么就会有多个线程同时创建LazySingleton_00实例。这样就导致了多次实例化。

案例2,使用synchronized对方法进行加锁后,在一个时间点只能有一个线程进入该方法,从而避免了类的多次实例化。

但是会发现一个问题,当一个线程进入该方法后,其他试图进入该方法的线程必须等待,即使已经被实例化了。这样就会导致性能问题,对象只创建一个,但是之后的使用都是需要进入synchronized方法。

3、懒汉模式-双重检查锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法,称为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public class LazySingleton_03 {

    private static LazySingleton_03 lazySingleton03 = null;

    private LazySingleton_03() {

    }

    public static LazySingleton_03 getLazySingleton03() {
        if (Objects.isNull(lazySingleton03)) {
            synchronized (LazySingleton_03.class) {
                if (Objects.isNull(lazySingleton03)){
                    // 1.分配空间  2.初始化 3.引用赋值
                    lazySingleton03 = new LazySingleton_03();
                }
            }
        }
        return lazySingleton03;
    }

}

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()行代码,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  • 给 instance 分配内存
  • 调用 Singleton 的构造函数来初始化成员变量
  • 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

4、懒汉模式-双重检查锁+volatile

public class LazySingleton_04 {

    private volatile static LazySingleton_04 lazySingleton03 = null; //声明成 volatile

    private LazySingleton_04() {

    }

    public static LazySingleton_04 getLazySingleton03() {
        // 线程B执行到此处
        if (Objects.isNull(lazySingleton03)) {
            synchronized (LazySingleton_04.class) {
                if (Objects.isNull(lazySingleton03)){
                    // 1.分配空间  2.初始化 3.引用赋值
                    lazySingleton03 = new LazySingleton_04();
                    // 字节码层
                    // JIT , CPU 有可能对如下指令进行重排序
                    // 1 .分配空间
                    // 2 .初始化
                    // 3 .引用赋值
                    // 如重排序后的结果为如下
                    // 1 .分配空间
                    // 3 .引用赋值 如果在当前指令执行完,有其他线程来获取实例,将拿到尚未初始化好的 实例
                    //#############         线程A执行到这里            #################
                    // 2 .初始化
                }
            }
        }
        return lazySingleton03;
    }

}

这里使用volatile作用是:禁止指令重排序优化。这样可以保证对线程环境下也能够正常运行。

饿汉模式
public class HungrySingleton{

    private static HungrySingleton instance=new HungrySingleton();

    private HungrySingleton(){
        //防止反射攻击
        if (HungrySingleton.instance != null){
            throw new RuntimeException("单例不允许多个实例!");
        }
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

}

类加载的时候就创建对象,不管使不使用都会创建。

单例模式-静态内部类实现

静态内部类(Static Inner Class)在 Java 中的加载时机与其外部类不同。具体来说,静态内部类在第一次使用时才会被加载,这与外部类的加载时机无关。这种特性使得静态内部类常用于实现单例模式的延迟加载(Lazy Initialization)。

静态内部类的加载时机

1.外部类加载时,静态内部类不会被立即加载。静态内部类只有在第一次被访问其成员(如字段、方法)被使用时,才会进行加载和初始化。

2.静态内部类的加载遵循类加载器的规则,且仅在需要时才加载。这与外部类的加载是相互独立的,外部类的实例化、方法调用等操作不会触发静态内部类的加载。

public class InnerClassSingleton{

    public static void main(String[] args) {
        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        InnerClassSingleton instance1 = InnerClassSingleton.getInstance();
        System.out.println(instance == instance1);

    }

    //初始化之前不会加载静态内部类,是在调用get方法并且返回值的时候初始化。
    // 静态内部类,持有 InnerClassSingleton 的唯一实例
    private static class InnerClassHolder{
        // 静态变量,存放唯一的 instance 实例
        private static InnerClassSingleton instance= new InnerClassSingleton();
    }
    private InnerClassSingleton(){
        //防止反射攻击
        if (InnerClassHolder.instance != null){
            throw new RuntimeException("单例不允许多个实例!");
        }
    }
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;// 调用时,触发 InnerClassHolder 加载
    }
}

当InnerClassSingleton类被加载时,静态内部类InnerClassHolder没有被加载。只有当被调用getInstance()方法从而触发InnerClassHolder.instance时,内部类才会加载,这个时候就会对instance进行实例化。并且JVM保证了 静态内部类在第一次使用时才会被加载

也是懒加载的一 种形式。

单例模式-枚举实现

Java 枚举类型实际上是继承自 java.lang.Enum 的特殊类。每个枚举值(常量)是该枚举类型的一个实例,枚举实例的创建过程由 Java 编译器和 JVM 内部控制。在枚举被加载时,所有的枚举常量会被实例化,而每个枚举常量都是 final 和 static 的,因此只会被实例化一次。

public enum EnumSingleton {

    INSTANCE;
    
    public void print(){
        System.out.println(this.hashCode());
    }

    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.INSTANCE;
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        System.out.println(instance1 == instance);
      	EnumSingleton.INSTANCE.print();
    }
}
上一篇:linux笔记(SSH)


下一篇:STM32与openmv的串口通信