单例模式为什么使用双重检查机制?为什么用volatile修饰单例对象?

  • Post author:
  • Post category:其他




单例模式为什么使用双重检查机制?为什么用volatile修饰单例对象?

  • 为什么使用双重检查机制?
  1. 如果单例已经创建了,直接调用synchronized加锁会比较消耗性能。所以首先判断对象有没有创建,没有创建再加锁。

  2. 加锁为了只让一个线程去创建对象。第二层非空检查的原因是在同时多个线程调用时,A线程获得锁并创建成功实例,之后释放锁,前面一起竞争的B线程获得锁,首先判断非空,代表已经创建了,所以不会继续去创建实例。

  • 为什么单例对象要用volatile修饰?

    volatile是为了防止指令重排序带来的多线程问题。

创建一个对象这行代码可以分解成3个步骤:

memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory(); // 3.设施instance指向刚分配的内存地址

在一些JIT编译器上,2和3之间可能会发生重排序:

memory = allocate(); // 1.分配对象的内存空间
instance = memory(); // 3.设施instance指向刚分配的内存地址
ctorInstance(memory); // 2.初始化对象

在重排序后,在多线程中的问题来了:

image-20220323192812141

A线程在创建实例过程中将instance指向内存空间后(步骤3),此时install已经不为null,但实例还未初始化。此时!!! B线程在第一次检查instance是否为null时,为非空状态,此时访问到(返回)的是一个未初始化的对象。


  1. 基于volatile的解决方案:

禁止分配内存空间(步骤2)和初始化对象(步骤3)之间的指令重排序。避免了访问到未初始化对象的可能性。

public class Singleton {
    private Singleton() {
    }

    private volatile static Singleton instance;

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

  1. 基于类初始化的解决方案:

静态内部类在使用的时候才加载,起到了懒加载的作用。

JVM在执行类的初始化期间,会去获取一个锁(初始化锁)。这个锁可以同步多个线程对同一个类的初始化(执行类的静态初始化和初始化类中声明的静态字段,此过程中发生的重排序对其它线程不可见)。由JVM的类加载保证了多线程初始化的并发问题。

public class InstanceFactory {
    // 静态内部类持有一个实例
    private static class InstanceHolder {
		public static Instance instance = new Instance();
    }
    
    public static Instance getInstance () {
		return InstanceHolder.instance; // 这里将导致 InstanceHolder 类被初始化
    }
}


单例模式(为什么双重检查要用volatile?)

《并发编程的艺术》



版权声明:本文为qq_40996976原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。