设计模式 (三) - 单例模式

版权声明 :

本文为博主原创文章,如需转载,请注明出处(https://blog.csdn.net/F1004145107/article/details/99864004)

  • / 前言 /

           单例模式其实很简单,但是它涉及到了并发,所以为其学习增加了一些难度,本文会涉及到一些多线程的知识,因为我想大家也不愿意只知其然而不知其所以然

           话不多说,我们直接开始~

  • / 1 / 为什么要使用单例模式

           相信大家能看这篇博文或多或少应该都使用过单例模式,而使用的最大原因就是因为系统内只需要一个实例即可,多则无用,另一部分则是为了安全,因为外部无法获得单例的生成信息,并且无法通过正规途径来创建该单例的对象,这里的正规途径指的是非反射

           我们仔细想一下在Spring中我们是否直接通过@Autowired注解就可以引入一个对象,但前提是你只有一个对象,如果你有多个对象,Spring根本不知道你要引入的是哪个,所以Spring为了防止这种情况发生,交给Spring来创建出来的对象都是单例的

  • / 2 / 单例模式怎么用

    • 饿汉式

             饿汉式,顾名思义,看到吃的就迫不及待了,所以饿汉式的单例模式会在程序加载伊始就创建对象

      /**
       * @author wise
       */
      public class SingleMode {
          private static SingleMode singleMode;
      
          private SingleMode () {
              this.singleMode = new SingleMode();
          }
      
          public static SingleMode getInstance () {
              return singleMode;
          }
      }

             原则是这样是没有问题的,但是如果类似的对象比较多呢,比如100个,每个都在初始化时就创建对象,那会浪费很多堆空间,而且服务的启动速度也会降低,浪费了非常多的资源,这对于一个优秀的设计来说是不应该的,因此饿汉式在实践中很少看到,但是如果你的这个对象是必须会用到的,不妨使用饿汉式,因为饿汉式的好处在于在类的初始化过程中就已经创建了对象,不会收到多线程并发的影响,也就是它足够安全

    • 懒汉式

             大家都应该听说过懒加载的概念,例如Mybatis就可以设置懒汉式延迟加载,懒汉式和懒加载是一样的,在用到的时候再去创建对象,保证了不会有多余的资源被浪费,但问题也是很明显的,面对复杂的使用场景,总是会碰到意外

      • 基础的懒汉式(**)

        /**
         * @author wise
         */
        public class SingleMode {
            private static SingleMode singleMode;
        ​
            private SingleMode () {}
        ​
            public SingleMode getInstance () {
                if (null == singleMode) {            //步骤1
                    singleMode = new SingleMode();   //步骤2
                }
                return singleMode;
            }
        }

               此时有N个线程,A,B同时调用了getInstance()方法,A通过了步骤1的验证,此时正在步骤2创建对象,但是B同时走到了步骤1,A还没有创建完成对象,B就会走到步骤2去,这样就会创建俩个对象,最后singleMode变量对应的地址值就是B在堆空间中开辟的空间,为了解决这个问题,程序员们设计出了双重检查锁

               我们来看一下并发下的场景

               虽然我们在创建前进行了null值的判断,但是这只适用于单线程的环境下,一旦发生了并发就会在堆空间中开辟多块空间

               只有调用getInstance()时才会创建SingleMode的对象

      • 双重检查锁

        • 普通版本(**)

                 双重检查锁就是在创建对象之前再做一次null值判断,但是如果单纯的null值判断也没什么意义,又会陷入到一开始的问题,所以最后用到了synchronized来加锁

          /**
           * @author wise
           */
          public class SingleMode {
              private static SingleMode singleMode;
          ​
              private SingleMode () {}
          ​
              public static SingleMode getInstance () {
                  if (null == singleMode) {                   //步骤1
                      synchronized (SingleMode.class) {  
                          if (null == singleMode) {           //步骤2
                              singleMode = new SingleMode();  //步骤3
                          }
                      }
                  }
                  return singleMode;
              }
          }

                 然而这里还是会出现一个问题,就是你拿到的对象不一定初始化完成了,加入我们在创建对象的同时需要初始化一大堆属性,按照正常情况下来说创建了对象之后然后赋值是没有问题的,但是JVM在实际运行中可能会为了提高性能而对代码的顺序进行重排序,当然重排序的的执行结果与顺序执行的结果是一致的,但是这种一致保证仍然是单线程下的        

                 我们来描述一下多线程下的情况

                 在此之前我们先来了解一下JVM创建对象的过程

                 1.先在堆中分配空间

                 2.初始化构造器

                 3.将对象的引用地址值存入到栈中的变量中 

                 此时有N个线程,线程A最先调用了getInstance()获取到了锁走到了步骤3,我们都知道对象的创建有3步我们假设说因为指令重排序,A线程先执行了1和3,singleMode对象就不为null了,因为这个变量已经有其堆空间的地址值了,但是A线程创建的对象此时还没有完成属性的设置,此时B线程执行到了步骤1,一判断singleMode对象不为null了,就直接取走了,但是此时singleMode对象还没有完成属性的初始化,B线程如果操作属性的话就可能会抛出异常

        • Volatile版本(****)

              Volatile是Java中的修饰符之一,只有成员变量可以使用,主要特性是可见性,有序性(防止重排序),简单来说使用了Volatile之后对当前变量的所有操作凡是涉及该变量的线程都能看到,感兴趣的话可以看我的另一篇博客Java多线程系列 - Volatile

          public class SingleMode {
          ​
              private volatile static SingleMode singleMode;
            
              private SingleMode () {}
              public static SingleMode getInstance () {
                  if (null == singleMode) {                   //步骤1
                      synchronized (SingleMode.class) {
                          if (null == singleMode) {           //步骤2
                              singleMode = new SingleMode();  //步骤3
                          }
                      }
                  }
                  return singleMode;
              }
          }

                 我们接着普通版本的故事继续说,此时因为singleMode对象使用了Volatile修饰符,所以不会存在因为重排序而产生的后续问题,即是产生了其它问题,也会在有线程修改singleMode变量后而通知到所有涉及该变量的线程

      • 静态内部类(*****)

               我们都知道类是只会被加载一次的,不管你是手写类加载器重复加载它都只会加载一次(双亲委托机制),我们可以利用这一点来生成我们要生成的对象,但是我们怎么实现懒加载呢,这时候就轮到内部类出现了,内部类有一个特性就是在加载外部类时内部类并不会被加载,只有被调用时才会加载,这样我们就可以实现懒汉式的单例,话不多说,看代码

        /**
         * @author wise
         */
        public class SingleMode {
        ​
            private SingleMode () {}
        ​
            public static SingleMode getInstance () {
                return SingleModeHolder.instance;
            }
        ​
            //内部类
            private static class SingleModeHolder {
                public static SingleMode instance = new SingleMode();
            }
        }

               静态内部类是比较完美的单例懒汉式,简单高效,巧妙的利用了类加载机制来完成了只会生成一个对象、懒加载,推荐大家使用

      • Enum枚举 (*****)

               我们都知道枚举是JDK1.5新加的一个了类,只要在类的申明上面使用了enum关键字,那么JVM在加载的时候会自动为当前类继承java.lang.Enum类,我们看一下申明一个枚举

        /**
         * @author wise
         */
        public enum  SingleMode {
            INSTANCE;
        ​
        }

               我们再来看一下枚举类的反编译代码,果然自动继承了java.lang.Enum,并且设置了泛型,可以看到我们在枚举类中申明的INSTANCE变成了被static final修饰的常量了,也就是说INSTANCE会在类被加载的时候就创建,而我们知道在类加载的时候线程是绝对安全的,也就能保证整个系统中只有一个INSTANCE对象了,但是这样会有一个弊端,我们无法实现懒加载了,无法实现懒加载也就意味着我们会浪费掉一部分资源

      • public final class com.example.demo.SingleMode extends java.lang.Enum<com.example.demo.SingleMode> {
          public static final com.example.demo.SingleMode INSTANCE;
        ​
          public static com.example.demo.SingleMode[] values();
        }
               但是枚举也是有好处的,枚举自动实现了序列化和比较接口,在需要该功能时我们只需要直接使用即可
        public abstract class Enum <E extends java.lang.Enum<E>> implements java.lang.Comparable<E>, java.io.Serializable {}
      • 饿汉式总结

        • 线程不安全的 : 普通版本懒汉式,双重检查锁

        • 线 程 安 全 的 : 静态内部类,枚举

        • 关于静态内部类和枚举

          • 其实我们在上面有一点没有介绍到,那就是反射,除了枚举之外,其它都是可以通过反射强行创建对象的,这样我们就无法保证系统内部只有一个对象了,但是枚举可以,枚举会直接抛出异常java.lang.NoSuchMethodException,所以我们在创建单例模式时要根据场景来进行取舍,如果需要的是绝对安全那可以选择枚举,如果更希望节约资源那就选择静态内部类

    • / 3 / 结语

             其实说来说去,单例模式的作用只有一个,那就是保证在系统内只存在一个该对象,那么它的使用场景在哪呢,例如创建线程池的工具类我们就可以设计成单例的,但是还是存在缺陷的,例如反射的存在,但是这种事情的发生性是比较低的,暂且可以忽略不计

             我们也可以看到单例模式其实也是一直在进化的,从最基本的版本到静态内部类、枚举,这是一个思考的过程,我们更需要的这种思路而不是实际的代码,希望本文能在设计模式之外带给你更多的东西

                     单例模式到此结束 ~

原创文章 42 获赞 51 访问量 1万+

猜你喜欢

转载自blog.csdn.net/F1004145107/article/details/99864004