"Double-Checked Locking"失效问题详解

Double-Checked Locking原本是为了解决多线程场景下的懒加载问题。但是当用Java实现时,如果不用额外的同步而造成的结果是不稳定的。总结原因有如下几个点:

1) 由于多线程同时进入临界区,造成重复创建单例对象

2) 由于编译器和处理器的指令重排,造成将未完全初始化的对象暴露出去

实现懒加载的解决方案:

1) 将成员变量声明为static,不用double-checked

2) 将成员变量声明为volatile,使用double-checked

首先看个例子:

class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }
并发场景下很显然会有多个线程会同时进入helper = new Helper()这行代码,从而导致会初始化多个helper对象。一个改进版本如下:

class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }
即每次进入函数getHelper()都需要加锁,显然这也违反了double-check的初衷:避免多线程懒加载初次判断的时候同步加锁。另一个改进版本为:

class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) 
      synchronized(this) {
        if (helper == null) 
          helper = new Helper();
      }    
    return helper;
    }
  // other functions and members...
  }
即首次判断不加锁,再次判断的时候需要加锁。很不幸由于编译器优化和多处理器的原因,该版本也无效。
上述代码无法正常运行的第一个原因是由于指令重排造成的,由于Helper()对象的初始化和对于helper私有变量的赋值可以被编译器或者多核处理器下的多进程重新调序执行,由此造成的结果是,会将一个没有初始化完成的Helper对象暴露给其他线程。

为了修复乱序的问题,又有人想出了如下方案:

class Foo { 
  private Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      Helper h;
      synchronized(this) {
        h = helper;
        if (h == null) 
            synchronized (this) {
              h = new Helper();
            } // release inner synchronization lock
        helper = h;
        } 
      }    
    return helper;
    }
  // other functions and members...
  }
其想法是将对象尽量晚一点暴露出去,期望在为helper变量复制和初始化Helper对象之间用锁的方式加一层内存屏障,从而方式强制防止编译器和处理器乱序执行。

然而上述方案仍然无效,原因是该方案错误理解了monitorexit的语义。monitorexit的语义是保证monitorexit之前的指令一定会在monitorexit执行之前(锁释放)被执行,而无法保障monitorexit语句之后指令不会提前到锁释放之前执行。换句话说,synchronize只保证同步块内的语句不会被调序出同步块之后执行,而不保证同步块之后的代码不会提前进入同步块执行。即monitorenter和monitorexit的内存屏障是单向的,而不是双向。即使有办法实现双向内存屏障也不要这么干,因为一旦Java内存模型修改,程序的正确性将无法得到保障。

那到底有没有正确实现懒加载的方案呢?有!方案一:

class HelperSingleton {
  static Helper singleton = new Helper();
  }
Java静态变量的语义保证了声明为静态变量的单例对象只有在第一次被访问时才会加载并初始化,而且会保证一定拿到的是一个被完整初始化的对象。

虽然double-check无法保证对象引用的正确多线程懒加载,但是对于32位的原始类型却可以正确生效。

class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) 
    synchronized(this) {
      if (cachedHashCode != 0) return cachedHashCode;
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }
原因很显然,32为原始类型的赋值操作是原子的。而由于64位的long型和double类型操作不是原子的,故此方法无效。如果computeHashCode()每次的计算值都一样,你甚至可以去去掉同步:

class Foo { 
  private int cachedHashCode = 0;
  public int hashCode() {
    int h = cachedHashCode;
    if (h == 0) {
      h = computeHashCode();
      cachedHashCode = h;
      }
    return h;
    }
  // other functions and members...
  }
JDK5扩展的volatile的语义,为实现double-check的懒加载提供了另一种方案:

class Foo {
        private volatile Helper helper = null;
        public Helper getHelper() {
            if (helper == null) {
                synchronized(this) {
                    if (helper == null)
                        helper = new Helper();
                }
            }
            return helper;
        }
    }
volatile的新语义为:

1) 不允许对于volatile的写操作与其之前的任何读/写操作调序

2) 不允许对于volatile的读操作与其之后的任何读/写操作调序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
default-checked-keys是一个Vue Tree组件的配置项,当设置为一组初始的选中节点时,这些节点在组件初始化时会被默认选中。但是,我们有时需要在操作过程中动态改变这些选中节点。 首先,要理解default-checked-keys的原理,它实际上是一个数组,包含了组件中的节点的key属性值,用于标识该节点。当组件初始化时,根据default-checked-keys中的key值来寻找对应的节点,并设置为选中状态。因此,要动态改变当前选中的节点,我们需要对default-checked-keys数组进行修改,并通知组件重新渲染。 在Vue中,我们可以通过以下几种方式来修改default-checked-keys数组: 1. 直接修改:可以使用数组的push、splice等方法来添加或删除default-checked-keys中的元素。但是注意,这种方式只能修改数组本身,不会通知组件进行重新渲染。 2. 赋值修改:直接将一个新数组赋值给default-checked-keys即可。这种方式会触发组件重新渲染,但是会丢失之前选中状态以外的所有节点的状态。 3. 深拷贝修改:对default-checked-keys进行深拷贝,并对拷贝的数组进行修改,再将拷贝数组赋值给default-checked-keys。这种方式可以保留之前选中状态以外的所有节点的状态。但是,不管哪种方式,修改完default-checked-keys后,都需要手动调用组件的setCheckedKeys方法来重新设置选中状态。 综上所述,动态改变default-checked-keys的关键是要理解其原理,并注意如何触发组件的重新渲染。随着Vue技术的不断发展,相信未来会有更加便捷的方式来实现这一功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值