Double-Lock Checking

本文探讨了双锁检测(Double-Lock Checking)在Java中的应用,特别是在单例模式下的实现方式,以及如何解决多线程环境下对象初始化的问题。此外,还介绍了双锁检测在ConcurrentHashMap的putIfAbsent方法中的应用,并提供了线程安全的替代方案。

Double-Lock Checking,也就是所谓的“双锁检测”,是实现Java并发安全的一个重要手段。比如单例模式的双检锁,先看下面代码:

class Foo { 
    private Helper helper = null;
    public Helper getHelper() {
    if (helper == null) 
         helper = new Helper();
         return helper;
    }
   // other functions and members...
}

这个在多线程状态下可能会生成多个Helper()实体,故不能正常工作。修改如下:

class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
}

这个虽然可以正常工作,但是效率太低,因为我们的目的仅仅是在初始化第一个Helper对象的时候需要加锁,其他时候根本不用。因此就有了第三种方法:

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=new Helper()这里。helper=表示给新的实体分配内存,然后再调用Helper()的构造函数来初始化helper成员变量。那么线程A和B在调用getHelper方法,A先进入,刚分配完内存,还没来得及调用构造函数就被踢出了CPU,然后B再进入,发现helper!=null,开始调用return helper,但是返回的却是一个没有初始化的对象(当然基础类型比如int就没有这个问题,因为不是类,不存在调用构造函数这一步)。可以用volatile关键字来解决(Java 5以后适用),也可以将helper声明为一个独立类的一个成员变量,即

class HelperSingleton {
static Helper helper= new Helper();
}

因为java的Lazy initiazation(即使用时才初始化)特性,而这个会在类加载时就初始化。而采用volatile关键字解决的代码如下:

class Foo {
    private volatile Helper helper;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}

这段代码加入result的含义是在helper已经初始化的情况下,保证对volatile helper变量只访问一次,因为返回的是result而不是helper,所以能获得25%的性能提升。


双锁检测还有一个应用的地方是在cocurrentHashmap里的putIfAbsent方法里,该方法的实现如下:

    default V putIfAbsent(K key, V value) {
        V v = get(key);
        if (v == null) {
            v = put(key, value);
        }

        return v;
    }

一目了然,如果对应的K没有V,就将V放进map去。所以应用的时候一定要小心。很多人不理解这个函数,将V设置为一个new Object(),导致每次都要初始化,比如这样:
map.putIfAbsent(name, new HashSet<X>());

其实本意是如果name有对应的HashSet就不初始化了,但很遗憾putIfAbsent函数的实现告诉我们这是不可能的,那么如何解决呢?可以用如下的双锁检测代码:

Set<X> set = map.get(name);
if (set == null) {
    final Set<X> value = new HashSet<X>();
    set = map.putIfAbsent(name, value);
    if (set == null) {
        set = value;
    }
}

这里一定要注意第二个if(set==null),千万不能丢。具体原理如何我也不太懂,以后再研究。当然JDK 1.8里提供了一个新的computeIfAbsent()函数,可以很好的实现线程安全,替代了putIfAbsent()这个让无数程序员跳坑的坑爹函数,实现如下:

Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>());
computeIfAbsent函数内部的实现原理这里不再详述,因为我现在也没看太懂.....





boolean isIndexDataFromSettingsAndOtherSuccess() { 1. thread1_checks_field: Thread1 uses the value read from static field mCollector in the condition com.android.settings.intelligence.search.indexing.DatabaseIndexingManager.mCollector == null. It sees that the condition is true. CID 205394: (#1 of 1): Check of thread-shared field evades lock acquisition (LOCK_EVASION) 5. thread2_checks_field_early: Thread2 checks mCollector, reading it after Thread1 assigns to mCollector but before some of the correlated field assignments can occur. It sees the condition com.android.settings.intelligence.search.indexing.DatabaseIndexingManager.mCollector == null as being false. It continues on before the critical section has completed, and can read data changed by that critical section while it is in an inconsistent state. Remove this outer, unlocked check of mCollector. 199 if (mCollector == null) { 2. thread1_acquires_lock: Thread1 acquires lock com.android.settings.intelligence.search.indexing.DatabaseIndexingManager.INDEX_SEARCH_OBJECT. 200 synchronized (INDEX_SEARCH_OBJECT) { 3. thread1_double_checks_field: Thread1 double checks the static field mCollector in the condition com.android.settings.intelligence.search.indexing.DatabaseIndexingManager.mCollector == null. 201 if (mCollector == null) { 4. thread1_modifies_field: Thread1 modifies the static field mCollector. This modification can be re-ordered with other correlated field assignments within this critical section at runtime. Thus, checking the value of mCollector is not an adequate test that the critical section has completed unless the guarding lock is held while checking. If mCollector is assigned a newly constructed value, note that the JVM is allowed to reorder the assignment of the new reference to mCollector before any field assignments that may occur in the constructor. Control is switched to Thread2.
最新发布
06-27
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值