暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

设计模式之单例模式

Java Miraculous 2021-09-09
347
  • 一、什么是单例?

某个类在一个JVM中只存在一个实例。
  • 二、什么场景下需要用到单例?

一些不需要多个实例的重量级的对象,比如线程池和数据库连接池等。
  • 三、怎么实现一个单例模式?

思路:
  • 提供一个该类的私有静态变量用来保存这个唯一的实例

  • 私有化构造方法,禁止外部通过new关键字调用

  • 提供一个公共的静态方法供外部获取私有的静态变量

实现:
    public class LazySingleton{


    private static LazySingleton instance;


    /**
    * 私有化构造方法,禁止外部调用
    */
    private LazySingleton() {
    }


    /**
    * 获取对象实例
    * @return
    */
    public static LazySingleton getInstance(){
    if (instance == null) {
    instance = new LazySingleton();
    }
    return instance;
    }
    }
    有问题吗?在单线程访问情况确实不会有问题,但是多线程情况下,可能会出现两个线程同时调用方法,结果就是new出了两个对象。
    • 改造1:加synchronized关键字

      public synchronized static LazySingleton getInstance(){
      if (instance == null) {
      instance = new LazySingleton();
      }
      return instance;
      }
      加锁就可以防止new出两个对象了,但是试想一下,如果这个对象已经存在了,实际运行时不会走new那一步的,只需要简单返回就可以了,这时候加synchronized是不是就降低了效率?
      • 改造2:双重校验锁

        public static LazySingleton getInstance(){
        if (instance == null) {
        synchronized (LazySingleton.class) {
        if (instance == null) {
        instance = new LazySingleton();
        }
        }
        }
        return instance;
        }


        主要是为了在对象存在的时候直接返回,而不用走加锁那一步了,那么这样的改造几乎是可行的,为什么说几乎?因为instance = new LazySingleton()这行代码的执行并不是原子操作,new对象的步骤可以用以下伪代码来表示:
        • memory = allocate();//1.分配对象内存空间 

        • instance(memory);//2.初始化对象 

        • instance = memory;//3.设置instance指向刚分配的内存地址,此时 instance != null

        由于编译器和处理器都可能执行指令重排优化,因此以上三个步骤有可能这样执行:
        • memory=allocate();//1.分配对象内存空间 

        • instance=memory;//3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成! 

        • instance(memory);//2.初始化对象

        指令重排只会保证串行语义执行的一致性(单线程),但并不会关心多线程间的语义一致性,因此在多线程情况下,某个线程判断不为null的实例可能是另外一个线程未初始化完成的对象,那怎么禁止指令重排,java里的volatile关键字即可。
        • 改造3:将变量用volatile修饰

          public class LazySingleton{


          **
          * 用volatile关键字禁止构造对象时候的指令重排
          */
          private volatile static LazySingleton instance;


          **
          * 私有化构造方法,禁止外部调用
          */
          private LazySingleton() {
          }


          **
          * 获取对象实例
          * @return
          */
          public static LazySingleton getInstance(){
          if (instance == null) {
          synchronized (LazySingleton.class) {
          if (instance == null) {
          instance = new LazySingleton();
          }
          }
          }
          return instance;
          }


          public static void main(String[] args) {
          LazySingleton.getInstance();
          }
          }
          到这里,如果你是正常使用,那么一点问题没有,但是我在文章开头说了:“禁止外部通过new关键字调用,其实构造一个对象,除了使用new关键字,还有反射,看下面的程序
            public static void main(String[] args) throws Exception {
            //利用反射调用构造方法创建对象
            Class<LazySingleton> classType = LazySingleton.class;
            //获取无参构造
            Constructor<LazySingleton> constructor = classType.getDeclaredConstructor(null);
            accessible属性设置为true就可以调用私有的构造函数,可以说是开挂了
            constructor.setAccessible(true);
            instance = LazySingleton.getInstance();
            LazySingleton instance2 = constructor.newInstance();
            System.out.println(instance == instance2);
            }

            构造方法私有化对反射来说形同虚设,这就好比外挂一般,不过既然它最终调用的是构造方法,我们可以在构造方法里阻击它。
            • 改造4:在私有构造中校验对象是否已经被创建

              /**
              * 私有化构造方法,禁止外部调用
              */
              private LazySingleton() {
              if (instance != null) {
              throw new RuntimeException("又想开挂?");
              }
              }

              这么容易的吗?魔高一尺,道高一丈,这句话反过来也成立,我改造下main方法
                public static void main(String[] args) throws Exception {
                //利用反射调用构造方法创建对象
                Class<LazySingleton> classType = LazySingleton.class;
                //获取无参构造
                Constructor<LazySingleton> constructor = classType.getDeclaredConstructor(null);
                accessible属性设置为true就可以调用私有的构造函数,可以说是开挂了
                constructor.setAccessible(true);
                // instance = LazySingleton.getInstance();
                LazySingleton instance2 = constructor.newInstance();
                LazySingleton instance3 = constructor.newInstance();
                System.out.println(instance3 == instance2);
                }

                既然你在私有构造里通过instance方法限制了我,那我不初始化instance,instance就永远是null,需要对象的时候我就反射,岂不是美滋滋,这这...怎么办?
                其实仔细想一下,归根结底是要让构造方法只调用一次,那是否可以设置一个标志,当调用完构造方法后改变标志的值,下次再来调的时候判断下就行了
                • 改造5:设置一个标志,用来标识构造方法是否被调用过

                  /**
                  * 设置对象实例化的标志
                  */
                  private static boolean init = false;


                  /**
                  * 私有化构造方法,禁止外部调用
                  */
                  private LazySingleton() {
                  synchronized (LazySingleton.class) {
                  if (!init) {
                  init = true;
                  }else {
                  throw new RuntimeException("小样,我还治不了你了");
                  }
                  }
                  }

                  小样,我还治不了你了?估计还真不太行啊,再改下main方法
                    public static void main(String[] args) throws Exception {
                    利用反射调用构造方法创建对象
                    Class<LazySingleton> classType = LazySingleton.class;
                    获取无参构造
                    Constructor<LazySingleton> constructor = classType.getDeclaredConstructor(null);
                    accessible属性设置为true就可以调用私有的构造函数,可以说是开挂了
                    constructor.setAccessible(true);
                            //在初始化instance前先反射调用一下
                    LazySingleton instance2 = constructor.newInstance();
                    System.out.println("instance2为:" + instance2);
                            //你会发现instance永远初始化不了了
                    instance = LazySingleton.getInstance();
                    System.out.println(instance);
                    }

                    其实这个问题和上面跳过instance初始化的原理一样,区别是只能让外部反射一次,到此离完美只差一步之遥,那就是想办法让instance先初始化,这样后面再想反射就不可能了,但是很遗憾的是,当前这种模式已经到极致了,其实大多数情况下单例都不会对外,不会有人去恶意攻击的,所以这种模式已经能应付大多数场景了。
                    附完整代码
                      package com.example.demo.designPatterns;


                      import java.lang.reflect.Constructor;


                      /**
                      * 懒汉式单例
                      * 只有在调用getInstance方法的时候,才会去创建
                      */
                      public class LazySingleton{


                      /**
                      * 用volatile关键字禁止构造对象时候的指令重排
                      */
                      private volatile static LazySingleton instance;


                      /**
                      * 设置对象实例化的标志
                      */
                      private static boolean init = false;


                      /**
                      * 私有化构造方法,禁止外部调用
                      */
                      private LazySingleton() {
                      synchronized (LazySingleton.class) {
                      if (!init) {
                      init = true;
                      }else {
                      throw new RuntimeException("小样,我还治不了你了");
                      }
                      }
                      }


                      /**
                      * 获取对象实例
                      * @return
                      */
                      public static LazySingleton getInstance(){
                      if (instance == null) {
                      synchronized (LazySingleton.class) {
                      if (instance == null) {
                      instance = new LazySingleton();
                      }
                      }
                      }
                      return instance;
                      }


                      public static void main(String[] args) throws Exception {
                      //利用反射调用构造方法创建对象
                      Class<LazySingleton> classType = LazySingleton.class;
                      //获取无参构造
                      Constructor<LazySingleton> constructor = classType.getDeclaredConstructor(null);
                      accessible属性设置为true就可以调用私有的构造函数,可以说是开挂了
                      constructor.setAccessible(true);
                      LazySingleton instance2 = constructor.newInstance();
                      System.out.println("instance2为:" + instance2);
                      instance = LazySingleton.getInstance();
                      System.out.println(instance);
                      }
                      }


                      那假如我的单例就是对外的,必须要解决上面的问题,怎么办?
                      这时候我们得转变思路了,逻辑不出来的时候要考虑发散下自己的思维,想一下java里有什么方式可以让instance先初始化,静态内部类知道吧?
                        package com.example.demo.designPatterns;


                        import java.lang.reflect.Constructor;


                        /**
                        * 懒汉式单例模式2
                        * 基于静态内部类实现
                        */
                        public class InnerClassSingleton {


                        **
                        * 静态内部类
                        */
                        private static class InnerClassHolder{
                        private static InnerClassSingleton instance = new InnerClassSingleton();
                        }


                        /**
                        * 私有化构造方法,禁止new关键字调用
                        */
                        private InnerClassSingleton() {
                        if (InnerClassHolder.instance != null) {
                        throw new RuntimeException("小样,你又来了?");
                        }
                        }


                        /**
                        * 提供一个方法用来获取实例
                        * @return
                        */
                        public static InnerClassSingleton getInstance(){
                        return InnerClassHolder.instance;
                        }


                        public static void main(String[] args) throws Exception {
                        //利用反射调用构造方法创建对象
                        Class<InnerClassSingleton> classType = InnerClassSingleton.class;
                        //获取无参构造
                        Constructor<InnerClassSingleton> constructor = classType.getDeclaredConstructor(null);
                        //accessible属性设置为true就可以调用私有的构造函数,可以说是开挂了
                        constructor.setAccessible(true);
                        try {
                        InnerClassSingleton instance2 = constructor.newInstance();
                        }catch (Exception e) {
                        e.printStackTrace();
                        }
                        InnerClassSingleton instance = InnerClassSingleton.getInstance();
                        System.out.println("instance:" + instance);
                        }
                        }


                        可以看到,上面先调用的反射,但是也不妨碍获取instance,为啥呢?看下构造函数中的判断,if (InnerClassHolder.instance != null),这行代码简单巧妙,因为它充分利用了静态内部类的特性:
                        • 外部类初次加载时不会加载静态内部类

                        • 类只会加载一次,不用考虑线程安全问题

                        if (InnerClassHolder.instance != null)运行的原理如下:
                        • 加载InnerClassHolder静态内部类,由于类的加载机制保证线程安全

                        • 加载InnerClassHolder的过程中会初始化instance,所以在和null比较之前,已经初始化好了instance,调用getInstance时只是获取了初始化好的instance实例

                        综上,静态内部类实现的单例模式可以完美解决反射攻击,而且在实际使用到的时候才会加载,也相当于懒汉式单例
                        上面实现的两种单例模式有一个共同点,就是真正去调用getInstance的时候才会实例化也就是真正在用到的时候才创建,如果这个对象很大,而且不一定用得到,那这种单例模式的优点就显示出来了。
                        如果这个对象在JVM启动后就要马上用到,那这时候我们不妨试试另外一种方式
                          package com.example.demo.designPatterns;


                          import java.lang.reflect.Constructor;


                          /**
                          * 饿汉式单例
                          */
                          public class HungrySingleton {


                          private static HungrySingleton instance = new HungrySingleton();


                          /**
                          * 私有化构造禁止外部调用
                          */
                          private HungrySingleton() {
                          if (instance != null){
                          throw new RuntimeException("你破解不了的");
                          }
                          }


                          public static HungrySingleton getInstance(){
                          return instance;
                          }


                          public static void main(String[] args) throws NoSuchMethodException {
                          //利用反射调用构造方法创建对象
                          Class<HungrySingleton> classType = HungrySingleton.class;
                          //获取无参构造
                          Constructor<HungrySingleton> constructor = classType.getDeclaredConstructor(null);
                          //accessible属性设置为true就可以调用私有的构造函数,可以说是开挂了
                          constructor.setAccessible(true);
                          try {
                          HungrySingleton instance2 = constructor.newInstance();
                          System.out.println("instance2:" + instance2);
                          }catch (Exception e) {
                          e.printStackTrace();
                          }
                          HungrySingleton instance = HungrySingleton.getInstance();
                          System.out.println("instance:" + instance);
                          }
                          }


                          当HungrySingleton类加载完成后就已经把instance实例化完成了,这种模式充分利用了类的加载机制来省去我们自己实现同步的部分。
                          ok,单例模式的实现方式到这已经差不多都介绍完了,but,你有没有发现上面的三种实现方式有一个共同点:都做了防止反射攻击的处理,有没有一种模式可以不用处理防反射呢?枚举
                            package com.example.demo.designPatterns;


                            public enum EnumSingleton {


                            INSTANCE;


                            public static void main(String[] args) {
                            System.out.println(EnumSingleton.INSTANCE.hashCode());
                            }
                            }


                            枚举的好处:
                            • 第一次真正被用到的时候,才会被虚拟机加载并初始化

                            • 类加载过程是由jvm保证线程安全的

                            • 天然不支持反射

                            当然,它也有缺点
                            • 不能被继承

                            • 属性必须在创建时指定, 相当于不能延迟加载

                            • 占用的内存空间是静态变量的2倍多

                            关于枚举的底层原理在这就不展开了,有兴趣的可以自己研究下。
                            单例模式就介绍到这里了,可以根据不同的场景选择合适的单例模式实现。
                            文章转载自Java Miraculous,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                            评论