彻底理解单例设计模式

单例设计模式

1. 单例模式概念

  • 单例模式(Singleton Pattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

2. 单例模式结构

  • 单例模式的主要有以下角色:
    • 单例类。只能创建一个实例的类
    • 访问类。使用单例类

3. 实现之一饿汉式

饿汉式:类加载就会导致该单实例对象被创建

  • 方式一:静态变量

    public class Singleton {
        
        //创建一个私有的无参构造方法
        private Singleton(){}
        
        //在成员位置创建该类的对象
        private static Singleton instance = new Singleton();
        
        //给外部提供一个静态方法获取该对象
        public static Singleton getInstance(){
            return instance;
        }
    
    }
    

    该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

  • 方式二:静态代码块的方式

    public class Singleton {
        
        //私有的无参构造方法
        private Singleton(){}
        
        //在成员位置创建该类的对象
        private static Singleton instance = null;
        
        //静态代码块
        static {
            instance = new Singleton();
        }
        
        //给外部提供一个静态方法获取该对象
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是随着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。

4. 实现之二懒汉式

懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

  • 方式一(线程不安全)

    public class Singleton {
        private Singleton(){}
    
        private static Singleton instance = null;
    
        public static Singleton getInstance(){
            //判断instance是否为空(是否为第一次创建)
            if (instance == null){
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    该方式在成员位置**声明**Singleton类型的静态变量,并没有进行对象的赋值操作。当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题

  • 方式二(加锁,线程安全)

    public class Singleton {
        private Singleton(){}
    
        private static Singleton instance = null;
    
        //加锁机制保证线程安全
        public synchronized static Singleton getInstance(){
            if (instance == null){
                instance = new Singleton();
            }
            return instance;
        }
    }
    

    该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效率特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

  • 方式三(DCL双重锁机制)

    public class Singleton {
        private Singleton(){}
    
        private static Singleton instance = null;
    
        public static Singleton getInstance(){
            //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
            if (instance == null){
                synchronized (Singleton.class){
                    //抢到锁后,第二次判断
                  if (instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作

  • 方式四(带有volatile的DCL单例模式,使用 volatile 关键字, volatile 关键字可以保证可见性有序性推荐使用

    public class Singleton {
        private Singleton(){}
    
        private static volatile Singleton instance = null;
    
        public static Singleton getInstance(){
            //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
            if (instance == null){
                synchronized (Singleton.class){
                    //抢到锁后,第二次判断
                    if (instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

  • 方式五(静态内部类方式)推荐使用

    public class Singleton {
    
        private Singleton(){}
    
        //静态内部类
        private static class SingletonBuilder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance(){
            return SingletonBuilder.INSTANCE;
        }
    }
    

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

    第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonBuilder并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性

    静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

5. 单例模式的破坏与解决方案

  • 上述创建单例模式的方式,有些是"安全"的,但所指的"安全"都是相对的,因为Java有个非常强大的机制:反射

  • 使用**序列化反射**可以让上面定义的单例类(Singleton)创建多个对象。

5.1 序列化反序列化

  • 演示序列化反序列化破坏单例模式

    //Singleton类
    public class Singleton implements Serializable {
        private Singleton() {
        }
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
        
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    
    public class DemoTest {
        public static void main(String[] args) throws Exception {
            //往文件中写对象
            writeObjectToFile();
            //从文件中读取对象
            Singleton instance1 = readObjectFromFile();
            Singleton instance2 = readObjectFromFile();
    	System.out.println(instance1 == instance2)//false
        }
    
        private static Singleton readObjectFromFile() throws Exception {
            //创建对象输入流对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\Users\\Desktop\\a.txt"));
            //第一个读取Singleton对象
            Singleton instance = (Singleton) ois.readObject();
            return instance;
    
        }
    
        private static void writeObjectToFile() throws Exception {
            //获取Singleton类的对象
            Singleton instance = Singleton.getInstance();
            //创建对象输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\Users\\Desktop\\a.txt"));
            //将instance对象写出到文件中
            oos.writeObject(instance);
        }
    
    }
    

    上面代码运行结果是false,表明序列化和反序列化已经破坏了单例设计模式

  • 解决方案:

    在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

    public class Singleton implements Serializable {
    
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
        
        /**
         * 下面是为了解决序列化反序列化破解单例模式
         * 加readResolve方法,方法名固定不可变
         */
        private Object readResolve() {
            return SingletonHolder.INSTANCE;
        }
    }
    
  • 源码解析:为什么readResolve()可以解决序列化破坏单例的问题?

    1. 我们从ObjectInputStream的readObject入手,readObject源码

      彻底理解单例设计模式_第1张图片

    2. readObject0源码:重点关注方法内的switch语句

      彻底理解单例设计模式_第2张图片

      checkResolve方法:检查对象并替换,readOrdinaryObject方法:读取二进制对象

    3. readOrdinaryObject源码:

      彻底理解单例设计模式_第3张图片

      可以看到,readOrdinaryObject()方法是通过desc.isInstantiable() 来判断是否需要new一个对象,如果返回true,方法通过反射的方式调用无参构造方法新建一个对象;如果返回false,返回null。

    4. isInstantiable源码:

      彻底理解单例设计模式_第4张图片

      cons是一个构造函数,cons ≠ null 是判断类的构造方法是否为空;但Class类的构造方法肯定不为空,所以isInstantiable()返回true,也就是说 obj 接收的一定是一个new的对象。

    5. readOrdinaryObject初始化后的源码:

      彻底理解单例设计模式_第5张图片
      .

      要让判断语句为true,hasReadResolveMethod是关键。

    6. hasReadResolveMethod源码

      彻底理解单例设计模式_第6张图片

      即readResolveMethod是否为null。

      彻底理解单例设计模式_第7张图片

      彻底理解单例设计模式_第8张图片

      对readResolveMethod赋值, 通过反射获得类中名为readResolve的方法。也就是说,如果目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类中的readResolve方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)

    7. 回到readOrdinaryObject初始化后的源码:

      彻底理解单例设计模式_第9张图片

      当整个判断语句为true时,执行Object rep = desc.invokeReadResolve(obj);

      invoke的意思是调用,看到这大家应该就明白了。

      通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量

      这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。

5.2 反射

  • 演示反射破坏单例模式

    public class Singleton {
    
        private Singleton() {
        }
    
        private static volatile Singleton instance = null;
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    public class DemoTest {
        public static void main(String[] args) throws Exception{
            //获取Singleton类的反射对象
            Class clazz = Singleton.class;
            //获取Singleton类的私有构造方法
            Constructor constructor = clazz.getDeclaredConstructor();
            //设置取消访问检查
            constructor.setAccessible(true);
    
            //通过构造方法创建Singleton类的对象实例
            Singleton instance1 = (Singleton) constructor.newInstance();
            Singleton instance2 = (Singleton) constructor.newInstance();
    
            System.out.println(instance1 == instance2);//false
        }
    }
    

    上面代码运行结果是false,表明反射已经破坏了单例设计模式

  • 解决方案:

    在无参构造函数中加入判断

    public class Singleton {
        private static boolean flag = false;
    
        private Singleton() {
            //加锁解决多线程不安全
            synchronized (Singleton.class){
                //判断flag的值,为false说明第一次访问,可以创建;为true说明不是第一次访问,不能创建
                if (flag) {
                    throw new RuntimeException("只能通过反射创建一次!");
                }
                //将flag的值设置为true
                flag = true;
            }
        }
    
        private static volatile Singleton instance = null;
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

6. 实现之三枚举类

  • 枚举类(枚举类也是一个类,继承了Enum)实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式

    public enum Singleton {
        INSTANCE;
    }
    
    public class DemoTest {
        public static void main(String[] args) {
            Singleton instance1 = Singleton.INSTANCE;
            Singleton instance2 = Singleton.INSTANCE;
            System.out.println(instance1 == instance2);//true
        }
    }
    
  • 思考:反射不能够破坏枚举类?换种说法,为什么不能通过反射创建枚举类对象?

    反射创建对象的过程:

    Class clazz = Singleton.class;
           Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
           constructor.setAccessible(true);
           Singleton singleton = (Singleton) constructor.newInstance();
    

    核心是通过构造器的newInstacne方法,newInstacne方法的源码:

    彻底理解单例设计模式_第10张图片

    如果反射对象是枚举类,则抛出IllegalArgumentException:无法通过反射创建枚举对象

你可能感兴趣的:(设计模式,设计模式,单例模式,java)