聊一聊开发中单例模式的双层检验锁

聊一聊开发中单例模式的双层检验锁


单例模式懒汉式普遍写法

public class SingleInstance {  
    private static SingleInstance instance;  

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

上面例子为懒汉式写法,看似没有问题,实则存在很大的bug
因此就有了双层检验锁,先上代码,后分析原理

双重验证锁

public class SingleInstance {

    private static volatile SingleInstance instance;

    public static SingleInstance getInstance() {
        //第一次校验
        if (instance == null) {
            synchronized (SingleInstance.class) {
                //第二次校验
                if (instance == null) {
                    instance = new SingleInstance();
                }
            }
        }
        return instance;
    }
}

那么为什么需要两次空判断呢?

第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance()方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的普通懒汉模式没什么区别,每次都要去竞争锁。

第二次校验:如果没有第二次校验,假设线程A执行了第一次校验后,判断为null,这时线程B也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来线程B获得锁,创建实例。这时线程A又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

接下来分析程序执行过程,看下是否完美


程序执行过程

双重校验锁方式的执行过程如下:

  1. 线程A进入 getInstance() 方法。
  2. 由于第一次校验 instance为 null,线程A向下执行进入 synchronized 块中。
  3. 此时线程B也进入 getInstance() 方法。
  4. 由于线程A还处于synchronized 块中未执行完成, instance仍然为 null线程B向下执行试图向下执行进入synchronized 块中。然而,由于线程A已经持有该锁,线程B在锁外阻塞等待。
  5. 线程A执行,由于在 第二次校验 处实例为 null,线程A创建一个 SingleInstance 对象并将其引用赋值给 instance。
  6. 线程A退出 synchronized 块并从 getInstance() 方法返回实例。
  7. 线程B获取锁向下执行 进入synchronized 块中并检查 instance 是否为 null。
  8. 由于 instance是非 null 的,并没有创建第二个 SingleInstance 对象,由线程A所创建的对象被返回。

看看执行流程,非常完美,似乎毫无破绽,可是


存在新的问题

以上双重检查锁执行流程理论是完美的。可是很不幸。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。

双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
instance = new SingleInstance();

该语句非原子操作,实际是三个步骤。

  1. 给instance 分配内存;
  2. 调用 SingleInstance 的构造函数来初始化成员变量;
  3. 将给instance 对象指向分配的内存空间(此时instance 才不为null);

虚拟机的指令重排序

执行命令时虚拟机可能会对以上3个步骤进行不规律的执行顺序 最后可能是132这种 分配内存并修改指针后未初始化 多线程获取时可能会出现问题。

线程A进入同步方法执行instance = new SingleInstance(); 代码时,恰好这三个步骤重排序后为1 3 2;
那么步骤3执行后 instance 已经不为null ,但是未执行步骤2,instance 对象初始化不完全,此时线程B执行getInstance() 方法,第一步判断时 instance 不为null,则直接将未完全初始化的 instance 对象返回了。

那么如何解决呢

如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的,同时还会禁止指令重排序

所以使用volatile关键字会禁止指令重排序,可以避免这种问题。使用volatile关键字后使得 instance = new SingleInstance();语句一定会按照上面拆分的步骤123来执行。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值