• 设计模式之单例模式(1)


    在这里插入图片描述
    Java单例类简单介绍了单例类,仔细分析其中的代码:

    class Singleton{
        private static Singleton instance;
        private Singleton(){}
        public static Singleton getInstance()
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }
    public class Hello {
        public static void main(String[] args)
        {
            Singleton instance1 = Singleton.getInstance();
            Singleton instance2 = Singleton.getInstance();
            System.out.println(instance1 == instance2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    乍一看,上面的单例似乎没有什么问题,运行结果是true。
    在这里插入图片描述

    但是如果换成多线程?这就不能保证是单例了。

    单例

    保证一个类只有一个实例,并提供一个访问它的全局访问点。
    在这里插入图片描述

    多线程引起的问题

    当在instance = new Singleton();加上断点,采用如下方式调用的时候,就会出现问题。

     public static void main(String[] args)
     {
         for (int i = 0; i < 1000; i++) {
             new Thread(new Runnable() {
                 @Override
                 public void run() { // anonymous class
                     Singleton s = Singleton.getInstance();
                     System.out.println(s.hashCode());
                 }
             }).start();
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    hashCode不一致,就是不同的对象,也就是说,这种方式无法保证对象只有一个。

    深层原因

    编译之后,通过javap -verbose Singleton查看Singleton字节码。

    D:\books>javap -verbose Singleton
    Classfile /D:/books/Singleton.class
      Last modified 2022年7月30日; size 356 bytes
      MD5 checksum bf33d3d37bf9439e50f687fa4d5cff42
      Compiled from "Test.java"
    class Singleton
      minor version: 0
      major version: 55
      flags: (0x0020) ACC_SUPER
      this_class: #3                          // Singleton
      super_class: #5                         // java/lang/Object
      interfaces: 0, fields: 1, methods: 2, attributes: 1
    Constant pool:
       #1 = Methodref          #5.#17         // java/lang/Object."":()V
       #2 = Fieldref           #3.#18         // Singleton.instance:LSingleton;
       #3 = Class              #19            // Singleton
       #4 = Methodref          #3.#17         // Singleton."":()V
       #5 = Class              #20            // java/lang/Object
       #6 = Utf8               instance
       #7 = Utf8               LSingleton;
       #8 = Utf8               
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               getInstance
      #13 = Utf8               ()LSingleton;
      #14 = Utf8               StackMapTable
      #15 = Utf8               SourceFile
      #16 = Utf8               Test.java
      #17 = NameAndType        #8:#9          // "":()V
      #18 = NameAndType        #6:#7          // instance:LSingleton;
      #19 = Utf8               Singleton
      #20 = Utf8               java/lang/Object
    {
      public static Singleton getInstance();
        descriptor: ()LSingleton;
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: getstatic     #2                  // Field instance:LSingleton;
             3: ifnonnull     16
             6: new           #3                  // class Singleton
             9: dup
            10: invokespecial #4                  // Method "":()V
            13: putstatic     #2                  // Field instance:LSingleton;
            16: getstatic     #2                  // Field instance:LSingleton;
            19: areturn
          LineNumberTable:
            line 6: 0
            line 8: 6
            line 10: 16
          StackMapTable: number_of_entries = 1
            frame_type = 16 /* same */
    }
    SourceFile: "Test.java"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    解释一下dup指令作用:也初始化指令会使当前对象的引用出栈,如果不复制一份,操作数栈中就没有当前对象的引用了,后面再进行其他的关于这个对象的指令操作时,就无法完成。

    在多线程情况下,第一个线程走完了3,进入了6,此时第二个线程走到3,由于初始化未完成,所以第二个线程依然会走6,这样就初始化了2次,对象就不一致了

    很明显,需要加锁。

    加锁

        public synchronized static Singleton getInstance()
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    让方法变成线程安全的。但是锁的范围有点大,于是就有了下面这种加锁方式,缩小锁的范围

        public  static Singleton getInstance()
        {
            if (instance == null)
            {
                synchronized(Singleton.class) {
                    instance = new Singleton();
                }
            }
            return instance;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可是在instance = new Singleton();中加入断点后发现对象不一致:
    在这里插入图片描述

    查看字节码内容如下:

    PS D:\books> javap -verbose Singleton
    Classfile /D:/books/Singleton.class
      Last modified 2022年7月30日; size 435 bytes
      MD5 checksum 2493cd5ece248c4395dd76e87508cd04
      Compiled from "Test.java"
    class Singleton
      minor version: 0
      major version: 55
      flags: (0x0020) ACC_SUPER
      this_class: #3                          // Singleton
      super_class: #5                         // java/lang/Object
      interfaces: 0, fields: 1, methods: 2, attributes: 1
    Constant pool:
       #1 = Methodref          #5.#18         // java/lang/Object."":()V
       #2 = Fieldref           #3.#19         // Singleton.instance:LSingleton;
       #3 = Class              #20            // Singleton
       #4 = Methodref          #3.#18         // Singleton."":()V
       #5 = Class              #21            // java/lang/Object
       #6 = Utf8               instance
       #7 = Utf8               LSingleton;
       #8 = Utf8               
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               getInstance
      #13 = Utf8               ()LSingleton;
      #14 = Utf8               StackMapTable
      #15 = Class              #22            // java/lang/Throwable
      #16 = Utf8               SourceFile
      #17 = Utf8               Test.java
      #18 = NameAndType        #8:#9          // "":()V
      #19 = NameAndType        #6:#7          // instance:LSingleton;
      #20 = Utf8               Singleton
      #21 = Utf8               java/lang/Object
      #22 = Utf8               java/lang/Throwable
    {
      public static Singleton getInstance();
        descriptor: ()LSingleton;
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=0
             0: getstatic     #2                  // Field instance:LSingleton;
             3: ifnonnull     31
             6: ldc           #3                  // class Singleton
             8: dup
             9: astore_0
            10: monitorenter
            11: new           #3                  // class Singleton
            14: dup
            15: invokespecial #4                  // Method "":()V
            18: putstatic     #2                  // Field instance:LSingleton;
            21: aload_0
            22: monitorexit
            23: goto          31
            26: astore_1
            27: aload_0
            28: monitorexit
            29: aload_1
            30: athrow
            31: getstatic     #2                  // Field instance:LSingleton;
            34: areturn
          Exception table:
             from    to  target type
                11    23    26   any
                26    29    26   any
          LineNumberTable:
            line 6: 0
            line 8: 6
            line 9: 11
            line 10: 21
            line 12: 31
          StackMapTable: number_of_entries = 2
            frame_type = 255 /* full_frame */
              offset_delta = 26
              locals = [ class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
            frame_type = 250 /* chop */
              offset_delta = 4
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    对于多线程,第一个线程执行完3的时候,第二个线程也执行3,然后第二个线程获取锁,实例化,然后第一个线程获取锁,再实例化。由此,产生了不同的对象。

    1. astore操作的index必须位于局部变量表中
    2. astore指令操作的是栈顶的returnAddress类型或reference类型的数
    3. astore用于弹出栈顶元素,赋值给局部变量(index)

    于是就诞生了著名的双检锁技术

    public  static Singleton getInstance()
    {
        if (instance == null)
        {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    自己码内容如下:

             0: getstatic     #2                  // Field instance:LSingleton;
             3: ifnonnull     37
             6: ldc           #3                  // class Singleton
             8: dup
             9: astore_0
            10: monitorenter
            11: getstatic     #2                  // Field instance:LSingleton;
            14: ifnonnull     27
            17: new           #3                  // class Singleton
            20: dup
            21: invokespecial #4                  // Method "":()V
            24: putstatic     #2                  // Field instance:LSingleton;
            27: aload_0
            28: monitorexit
            29: goto          37
            32: astore_1
            33: aload_0
            34: monitorexit
            35: aload_1
            36: athrow
            37: getstatic     #2                  // Field instance:LSingleton;
            40: areturn
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在锁之内再次判空,保证了只有一次实例话。由于Java中JIT的存在,所以需要把instance声明为private static volatile Singleton instance;

    当然单例还有其他写法,比如内部类,通过JVM保证线程安全,还可以使用枚举,既保证了线程安全,又防止了序列化。

    写在最后

    单例分在懒汉和饿汉模式,而存在线程不安全问题的只在懒汉模式出现。所以可以的话,用饿汉式就可以,避免了很多没必要的麻烦。

     private static  Singleton instance = new Singleton();
    
    • 1

    这中缺点就是即使不要要也会实例化,但大多数情况下不会差这一点的内存。

    鸿蒙系统中又很多地方使用单例(C++),而且还用还提供了一个模板类了,代码如下,其实它没有保证构造函数私有,不过这又有什么关系那,重要的是模式,而不是那个死板的定义,一个模板简化可多少的操作。

    template
    class Singleton : public NoCopyable {
    public:
        static T &GetInstance()
        {
            return instance_;
        }
    
    private:
        static T instance_;
    };
    
    template
    T Singleton::instance_;
    }
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    上面的双检锁技术依然存在问题,这个是Java的内存模型导致的,在并发的情况下依然不能保证只实例化一次。而C#没有这个问题,这个在下一篇文章中会细说。

    公众号

    更多内容,欢迎关注我的微信公众号: 半夏之夜的无情剑客。
    在这里插入图片描述

  • 相关阅读:
    2022强网杯web(部分)
    flink sql text to jobGraph
    项目人力资源管理
    JavaEE - CORS跨域
    连接工具和idea能查询出数据库数据,项目中查不到数据库数据:解决办法
    虚拟机搭建负载均衡,mysql主从复制和读写分离(四、搭建主从复制和读写分离)
    小红书和达人合作步骤是什么?对接达人合作流程分享
    TypeScript语法快速上手
    基于微信小程序驾校报名系统(微信小程序毕业设计)
    申请著作权的流程有哪些
  • 原文地址:https://blog.csdn.net/helloworlddm/article/details/126064106