Double-Lock Checking

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函数内部的实现原理这里不再详述,因为我现在也没看太懂.....





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值