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

发布于:2023-02-06 ⋅ 阅读:(2366) ⋅ 点赞:(2)

在这里插入图片描述
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);
    }
}

乍一看,上面的单例似乎没有什么问题,运行结果是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();
     }
 }

在这里插入图片描述
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."<init>":()V
   #2 = Fieldref           #3.#18         // Singleton.instance:LSingleton;
   #3 = Class              #19            // Singleton
   #4 = Methodref          #3.#17         // Singleton."<init>":()V
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               instance
   #7 = Utf8               LSingleton;
   #8 = Utf8               <init>
   #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          // "<init>":()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 "<init>":()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"

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

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

很明显,需要加锁。

加锁

    public synchronized static Singleton getInstance()
    {
        if (instance == null)
        {
            instance = new Singleton();
        }
        return instance;
    }

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

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

可是在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."<init>":()V
   #2 = Fieldref           #3.#19         // Singleton.instance:LSingleton;
   #3 = Class              #20            // Singleton
   #4 = Methodref          #3.#18         // Singleton."<init>":()V
   #5 = Class              #21            // java/lang/Object
   #6 = Utf8               instance
   #7 = Utf8               LSingleton;
   #8 = Utf8               <init>
   #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          // "<init>":()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 "<init>":()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
}

对于多线程,第一个线程执行完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;
}

自己码内容如下:

         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 "<init>":()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

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

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

写在最后

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

 private static  Singleton instance = new Singleton();

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

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

template<typename T>
class Singleton : public NoCopyable {
public:
    static T &GetInstance()
    {
        return instance_;
    }

private:
    static T instance_;
};

template<typename T>
T Singleton<T>::instance_;
}
}
}

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

公众号

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