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