恶汉式

线程安全,调用率高,但是不能延时加载,类初始化时,立即加载这个对象

  1. public class Demo01 {
  2. private static Demo01 instance = new Demo01();
  3. private Demo01() { }
  4. public static Demo01 getInstance() {
  5. return instance;
  6. }
  7. }

懒汉式 和 DCL(✨)

可以延时加载,存在线程问题,可以加锁,并且为了兼顾效率,再加一次判断,减少判断锁的次数,即双重锁定检查(DCL,Double Check Lock)

  1. public class Single {
  2. private static Single instance;
  3. private Single() { }
  4. public static Single getInstance() {
  5. if(instance == null){ // 线程二检测到instance不为空
  6. synchronized(Single.class){
  7. if (instance == null) {
  8. //线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)
  9. instance = new Single();
  10. }
  11. }
  12. }
  13. // 后面线程二执行时将引发:对象尚未初始化错误
  14. return instance;
  15. }
  16. }

看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个 if 中就返回了,因此不会走到同步块中,已经完美了吗?

如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在 JVM 执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

  1. 给 instance 实例分配内存,属性赋值默认值;
  2. 初始化 instance 的构造器,属性赋值为初始值;
  3. 将 instance 对象指向分配的内存空间(注意到这步时 instance 就非 null 了)

如果指令按照顺序执行倒也无妨,但 JVM 为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:

  1. 给 instance 实例分配内存;
  2. 将 instance 对象指向分配的内存空间(这步之后为非 null)
  3. 初始化 instance 的构造器

此时,当线程一执行第2步完毕,在执行第3步前,被切换到线程二上,此时对于线程二,instance 判断为非空,线程二直接来到return instance 语句,拿走 instance 然后使用,就自然会报错(对象未初始化)。

具体来说就是 synchronized 虽然保证了线程的原子性(即 synchronized 块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
image.png

解决这个问题的方法是:使用 volatile 禁止指令重排序。

synchronized 锁 + DCL(Double Check Lock)+ volatile

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

将变量 instance 使用 volatile 修饰 即可实现单例模式的线程安全。

静态内部类

优点

  • 基于类初始化,线程安全、调用率高,能延时加载。
  • 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 instance,故而不占内存

缺点:

  • 由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如 Context 这种参数,所以,我们创建单例时,可以在静态内部类与 DCL 模式里自己斟酌。

该解决方案的根本就在于:利用 classloder 的机制来保证初始化 instance 时只有一个线程。JVM 在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化

  1. public class Demo03 {
  2. private Demo03() { }
  3. private static class SingletonClassHolder {
  4. private static final SingletonClassHolder instance = new SingletonClassHolder();
  5. }
  6. public static SingletonClassHolder getInstacne() {
  7. return SingletonClassHolder.instance;
  8. }
  9. }

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

image.png
类加载时机:JAVA 虚拟机在有且仅有的 5 种场景下会对类进行初始化。

  • 遇到 newgetstaticsetstatic 或者 invokestatic 这4个字节码指令时。对应的 java 代码场景为:
    • new 一个关键字或者一个实例化对象时
    • 读取或设置一个静态字段时( final 修饰、已在编译期把结果放入常量池的除外)
    • 调用一个类的静态方法时。
  • 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没进行初始化,需要先调用其初始化方法进行初始化。
  • 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  • 当使用 JDK 1.7 等动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这 5 种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是”有且仅有”,那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

所以当 getInstance() 方法被调用时,SingleTonHoler 才在 SingleTon 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 INSTANCE 也真正被创建,然后再被 getInstance() 方法返回出去,这点同饿汉模式

枚举单利

线程安全、调用率高,推荐使用

  1. @ThreadSafe
  2. @Recommend
  3. public class Demo4 {
  4. // 私有构造函数
  5. private Demo4() {
  6. }
  7. public static Demo4 getInstance() {
  8. return Singleton.INSTANCE.getInstance();
  9. }
  10. private enum Singleton {
  11. INSTANCE;
  12. private Demo4 singleton;
  13. // JVM保证这个方法绝对只调用一次
  14. Singleton() {
  15. singleton = new Demo4();
  16. }
  17. public Demo4 getInstance() {
  18. return singleton;
  19. }
  20. }
  21. }