一起走进单例模式

一.介绍

单例模式(Singleton Pattern)属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象(不需要手动去new)。本文会结合源码加深你对单例模式的理解

二.实现方式

  1. 大体上分为四种实现方式:饿汉式、懒汉式、静态内部类、枚举类
  2. 实现思想基本一致:
    • 提供一个唯一的私有实例
    • 私有化构造器
    • 提供一个公共方法获取私有实例
  3. 饿汉式
/**
 * 饿汉式
 */
public class Demo1 {
    
    
    private static Demo1 instance = new Demo1();

    private Demo1(){
    
    }

    public static Demo1 getInstance(){
    
    
      return instance;
    }
}

在JVM里面这个类只会被实例化一次,实例化的过程由JVM保证线程的安全(JVM以同步的形式来完成类加载的整个过程)
4. 懒汉式

/**
 * 懒汉式(双重检测锁)
 */
public class Demo2 {
    
    
    /**
     * volatile禁止指令重排
     */
    private volatile static Demo2 instance;

    private Demo2() {
    
    }

    public static Demo2 getInstance() {
    
    
        //防止instance不为null的情况下,某一线程持有锁时间过长,导致其他线程长时间等待
        if (instance == null) {
    
    
            //synchronized关键字确保线程安全
            synchronized (Demo2.class) {
    
    
                if (instance == null) {
    
    
                    instance = new Demo2();
                }
            }
        }
        return instance;
    }
}
  1. 静态内部类
/**
 * 静态内部类
 */
public class Demo3 {
    
    
    private Demo3(){
    
    }

    private static class Inner{
    
    
        //将[提供一个唯一的私有实例的操作]放在了静态内部类
        private static Demo3 instance = new Demo3();
    }

    public static Demo3 getInstance(){
    
    
        return Inner.instance;
    }
}
  1. 枚举
/**
 * 枚举类
 */
public enum Demo4 {
    
    
    INSTANCE;
}

我们可以通过反编译Demo4的class文件看出,这种方式也无非就是提供了一个唯一的静态实例
在这里插入图片描述

三.单例模式的破坏与预防

  1. 序列化破坏单例
/**
 * 序列化破坏单例模式
 */
public class SerializeAttack {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Demo1 instance = Demo1.getInstance();
        
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("instance"));
        oos.writeObject(instance);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("instance"));
        Demo1 instance1 = (Demo1) ois.readObject();
        ois.close();
        //反序列化获取的对象与序列化前的对象不相同
        System.out.println(instance ==  instance1); //false
    }
}
  1. 序列化预防(枚举类天生具有)
    Serializable接口的源码中已经给出了解决方案-提供readResolve方法
    在这里插入图片描述

具体实现

Object readResolve() throws ObjectStreamException{
    
    
        return instance;
    }

为什么提供了readResolve方法就能预防序列化破坏?我们看一下readObject方法的源码
在这里插入图片描述
在这里插入图片描述
为什么枚举类天生预防反序列化?我们再看一下readObject方法的源码
在这里插入图片描述
在这里插入图片描述

  1. 反射破坏单例
/**
 * 反射破坏单例模式
 */
public class ReflectAttack {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Constructor<Demo1> constructor = Demo1.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        System.out.println(constructor.newInstance() == Demo1.getInstance());
    }
}
  1. 反射预防(枚举类天生具有,懒汉式无法预防)
    具体实现
//修改私有构造器的逻辑
private Demo1(){
    
    
        if(instance != null){
    
    
            throw new RuntimeException("prevent reflectAttack");
        }
    }

为什么枚举类天生预防反射攻击?我们来看下newInstance方法的源码
在这里插入图片描述

四.在JDK中的应用

单例模式在JDK中最经典的应用莫过于RunTime类(饿汉式)
在这里插入图片描述

五.在Spring中的应用

org.springframework.core.ReactiveAdapterRegistry(懒汉式)
在这里插入图片描述

六.饿汉模式的缺点(拓展)

饿汉模式无法控制只有在调用getIntance方法的时候才进行初始化(无法进行懒加载)
1.我们先来看下类加载过程

  • 加载二进制数据到内存,生成对应的Class数据结构
  • 连接:a.验证,b.准备(给类的静态成员变量赋默认值),c.解析
  • 初始化:给类的静态变量赋初值

2.什么时候会触发初始化?
当前类是启动类即main函数所在类、直接进行new操作、访问静态属性、访问静态方法、用反射访问类、初始化一个类的子类等

3.饿汉模式无法控制只有在调用getIntance方法的时候才进行初始化
这里采用访问静态属性的方式举例,并给饿汉式提供一个用于验证的静态变量name

SingleTon.java

/**
 1. 饿汉式
 */
public class SingleTon{
    
    
    //静态属性name,仅用于验证
    public static String name;

    private static SingleTon instance = new SingleTon();

    private SingleTon(){
    
    
        //控制台打印,仅用于验证
        System.out.println("初始化了");
    }

    public static SingleTon getInstance(){
    
    
      return instance;
    }
}

测试类SingletonTest.java

public class SingletonTest {
    
    
    public static void main(String[] args) {
    
    
        System.out.println(Demo1.name); //初始化了 null
    }
}

可以看出在未调用getIntance方法的时候,intance实例被初始化了

4.如何弥补这个缺点?
采用静态内部类的方式可以弥补这个缺点,我们来验证一下

SingleTon.java

/**
 * 静态内部类
 */
public class SingleTon{
    
    
    //静态属性name,仅用于验证
    public static String name;

    private SingleTon(){
    
    
        //控制台打印,仅用于验证
        System.out.println("初始化");
    }

    private static class Inner{
    
    
        private static SingleTon instance = new SingleTon();
    }

    public static SingleTon getInstance(){
    
    
      return Inner.instance;
    }
}

测试类SingletonTest.java

public class SingletonTest {
    
    
    public static void main(String[] args) {
    
    
        System.out.println(SingleTon.name); //null
    }
}

可以看出在访问外部静态变量的时候,instance实例没有被初始化

Guess you like

Origin blog.csdn.net/a347635191/article/details/121725621