Java 单例模式

饿汉模式

public class Singleton {
    private Singleton() {
        // 必须是私有构造方法
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
}

上面的写法最简单最有效并且是线程安全的(由JVM保证)。但是没有实现延迟初始化。
注意:

  1. 上面私有的构造方法必须要有,否则JVM会自动添加默认的构造方法,这个是public的。
  2. 不要将成员变量 instance 设置为public的,这违反了成员私有的原则。而且 instance 成员变量必须是 static 的。如果不是 static 的,getInstance方法无法访问非静态成员。 static关键字也满足了只有一个实例的的语义。
  3. 成员方法必须是 static 的。否则必须要有 Singleton 对象才能调用,这是一个悖论。本来 getInstance 方法就是要拿到 Singleton 对象。

不延迟加载有什么问题?

  1. 如果不延迟加载的话,在 JVM 加载 Singleton 这个类时,由于 instance 是 static 的,所以会进行初始化(类加载过程中的初始化阶段),new的过程很耗时的话(可能有关于对象的其他初始化操作),应用程序启动的会很慢,降低体验的友好性。
  2. 如果不延迟加载的话,假设 Singleton 这个类还有其他的功能,如提供了public static void sayHello()的方法,当初次调用 sayHello 方法时,会实例化 Singleton 对象。然而,在这里可能并用不到这个对象,造成了浪费。
    注意,只有当应用程序访问到 Singleton 时,类加载器才会加载 Singleton class。
  3. 静态的成员变量肯定是在类加载时就进行了初始化操作。

懒汉式,线程不安全

class Singleton {
    private Singleton() {
        //must
    }

    private static Singleton instance = null;

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

这段代码使用了懒加载模式,但是却存在致命的问题。当有多个线程同时调用 getInstance() 的时候,就会创建多个实例(同时判定instance == null)。也就是说在多线程下不能正常工作。

懒汉式,线程安全

public class Singleton {
    private Singleton() {
        //must
    }

    private static Singleton instance = null;

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

注意:

  1. instance = null; 这样即使加载了类,也不会造成创建对象的耗时。
  2. synchronized关键字的使用。如果没有 synchronized 关键字,在高并发情况下,会new出多个instance实例。

上面的代码实现了延迟加载。但是存在一些问题。如果 getInstance 的调用频率很高的话,每次都 synchronized 同步访问,效率岂不是很低。因此,要降低锁的力度。

双重检查锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public class Singleton {
    private Singleton() {
        //must
    }

    private static Singleton instance = null;

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

上面的代码其实就是大名鼎鼎的DCK了。这个大大提高了并发访问性能,而且实现了延迟初始化。如果第一个 instance 不为 null 的话,就可以直接返回了,减少了同步开销。只有当 instance 是 null 的时候,才会进行同步操作。这个时候需要在进行一次 instance 是否为 null 的检查。因为,有可能两个线程都判断 instance 为 null,一个线程加锁,对instance进行了实例化,释放锁,另外一个线程拿到锁后,不进行 instance 是否为 null 的再次判断,会再次进行instance的实例化。
上面代码有一个问题:instance 不是 volatile 的。看 new 操作的反汇编代码,其实他包含3条汇编指令:new、dup、init。这样当一个线程执行了new汇编指令后,被唤出。一个新的线程到来,在第一个判断 instance 是否为null时,发现 instance 已经不为null了,直接返回。这时的instance其实是不完整的。而且,即使一个线程实例化了 instance,由于每个线程都有自己的working 缓存,可能另一个线程看不到前一个线程对 instance 的操作。

new 字节码对应的处理流程:

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量(invokespecial)
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

volatile 实现

public class Singleton {
    private Singleton() {
        //must
    }

    private static volatile Singleton instance = null;

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

JDK1.5 及之后版本增加了 volatile 关键字的一个语义:禁止指令重排序优化。

volatile 的两层语义:

  • 可见性
    可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。
  • 禁止指令重排序优化

静态内部类实现

public class Singleton {
    private Singleton() {
        //must, otherwise JVM create default constructor(public)
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
}

内部的静态类 SingletonHolder,只有当 getInstance 方法被调用时才会被加载。注意 SingletonHolder 这个内部类是 static 的,否则其成员变量不能声明成 static 的。JVM在类加载时保证了线程安全性问题,以及 instance 的唯一性。
说到底就是 JVM 本身保证了只能有一个线程能够完成对某个类的加载,也即 JVM 中仅有一个该类的实例(Class的实例)。

枚举实现

上面提到的几种实现单例的方式都有共同的缺点:
1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

public enum Singleton {
    INSTANCE
}

我们可以通过 Singleton.INSTANCE 来访问实例,这比调用 getInstance() 方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。

javap -v  Singleton
Classfile /Users/n3verl4nd/Desktop/Singleton.class
  Last modified 2018-8-8; size 730 bytes
  MD5 checksum 4820457d608bac02cd356d7af672026d
  Compiled from "Singleton.java"
public final class Singleton extends java.lang.Enum<Singleton>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
   #1 = Fieldref           #4.#29         // Singleton.$VALUES:[LSingleton;
   #2 = Methodref          #30.#31        // "[LSingleton;".clone:()Ljava/lang/Object;
   #3 = Class              #14            // "[LSingleton;"
   #4 = Class              #32            // Singleton
   #5 = Methodref          #10.#33        // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
   #6 = Methodref          #10.#34        // java/lang/Enum."<init>":(Ljava/lang/String;I)V
   #7 = String             #11            // INSTANCE
   #8 = Methodref          #4.#34         // Singleton."<init>":(Ljava/lang/String;I)V
   #9 = Fieldref           #4.#35         // Singleton.INSTANCE:LSingleton;
  #10 = Class              #36            // java/lang/Enum
  #11 = Utf8               INSTANCE
  #12 = Utf8               LSingleton;
  #13 = Utf8               $VALUES
  #14 = Utf8               [LSingleton;
  #15 = Utf8               values
  #16 = Utf8               ()[LSingleton;
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               valueOf
  #20 = Utf8               (Ljava/lang/String;)LSingleton;
  #21 = Utf8               <init>
  #22 = Utf8               (Ljava/lang/String;I)V
  #23 = Utf8               Signature
  #24 = Utf8               ()V
  #25 = Utf8               <clinit>
  #26 = Utf8               Ljava/lang/Enum<LSingleton;>;
  #27 = Utf8               SourceFile
  #28 = Utf8               Singleton.java
  #29 = NameAndType        #13:#14        // $VALUES:[LSingleton;
  #30 = Class              #14            // "[LSingleton;"
  #31 = NameAndType        #37:#38        // clone:()Ljava/lang/Object;
  #32 = Utf8               Singleton
  #33 = NameAndType        #19:#39        // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  #34 = NameAndType        #21:#22        // "<init>":(Ljava/lang/String;I)V
  #35 = NameAndType        #11:#12        // INSTANCE:LSingleton;
  #36 = Utf8               java/lang/Enum
  #37 = Utf8               clone
  #38 = Utf8               ()Ljava/lang/Object;
  #39 = Utf8               (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
{
  public static final Singleton INSTANCE;
    descriptor: LSingleton;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static Singleton[] values();
    descriptor: ()[LSingleton;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field $VALUES:[LSingleton;
         3: invokevirtual #2                  // Method "[LSingleton;".clone:()Ljava/lang/Object;
         6: checkcast     #3                  // class "[LSingleton;"
         9: areturn
      LineNumberTable:
        line 1: 0

  public static Singleton valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)LSingleton;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #4                  // class Singleton
         2: aload_0
         3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #4                  // class Singleton
         9: areturn
      LineNumberTable:
        line 1: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #4                  // class Singleton
         3: dup
         4: ldc           #7                  // String INSTANCE
         6: iconst_0
         7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        10: putstatic     #9                  // Field INSTANCE:LSingleton;
        13: iconst_1
        14: anewarray     #4                  // class Singleton
        17: dup
        18: iconst_0
        19: getstatic     #9                  // Field INSTANCE:LSingleton;
        22: aastore
        23: putstatic     #1                  // Field $VALUES:[LSingleton;
        26: return
      LineNumberTable:
        line 2: 0
        line 1: 13
}
Signature: #26                          // Ljava/lang/Enum<LSingleton;>;
SourceFile: "Singleton.java"

http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
https://blog.csdn.net/fan2012huan/article/details/53454724
https://blog.csdn.net/goodlixueyong/article/details/51935526

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

N3verL4nd

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值