转载自:http://blog.youkuaiyun.com/iter_zc/article/details/40543365
在业务上可能会遇到有这种场景,全局维护一个并发的ConcurrentMap, Map的每个Key对应一个对象,这个对象需要只创建一次。如果Map中该key对应的value不存在则创建,否则直接返回。
通常会使用ConcurrentHashMap的putIfAbsent(key, value) 方法来保证并发Map里面的这个key只有唯一的值。但是这种做法会造成某些线程无谓地创建了对象,尽管最后放入Map的是唯一的值。
public Registry getRegistry(String registryAddress){
Registry registry = registries.get(registryAddress);
if(registry == null){
// 第一次并发时可能有多个线程进入到这块代码,这些线程都执行创建对象
registry = createRegistry();
// 利用并发Map的putIfAbsent保证只有第一次创建的对象被放入Map
registries.putIfAbsent(registryAddress, registry);
// 再次获取一下,这时候是肯定不为空
registry = registries.get(registryAddress);
}
return registry;
}
这里registry可能会被多个线程创建出来,而且当createRegistry()是个很重的方法时,这种方法显然不是最优的。于是有了以下的改进:
// 记录自旋状态的轻量级类,只封装了一个volatile状态
public static class SpinStatus{
volatile boolean released;
}
// 辅助并发控制的Map,用来找出每个key对应的第一个成功进入的线程
private ConcurrentMap<String, SpinStatus> raceUtil = new ConcurrentHashMap<String, SpinStatus>();
// 无锁实现单实例Registry的创建
public Registry getRegistry(String registryAddress){
Registry registry = registries.get(registryAddress);
// 第一次创建
if(registry == null){
// 需要为并发的线程new一个自旋状态,只有第一个成功执行putIfAbsent方法的线程设置的SpinStatus会被共享
SpinStatus spinStatus = new SpinStatus();
SpinStatus oldSpinStatus = raceUtil.putIfAbsent(registryAddress, spinStatus);
//只有第一个执行成功的线程拿到的oldSpinStatus是null,其他线程拿到的oldSpinStatus是第一个线程设置的,可以在所有线程中共享
if(oldSpinStatus == null){
// 创建对象
registry = createRegistry();
// 放入共享的并发Map,后续线程执行get()方法后可以直接拿到非null的引用返回
registries.put(registryAddress, registry);
// 释放其他自旋的线程,注意,对第一个成功执行的线程使用的是spinStatus的引用
spinStatus.released = true;
}else{
// 其他线程在oldSpinStatus引用所指向的共享自旋状态上自旋,等待被释放
while(!oldSpinStatus.released){};
}
// 再次获取一下,这时候是肯定不为空
registry = registries.get(registryAddress);
}
return registry;
}
上面的问题,ConcurrentHashMap#computeIfAbsent可以解决。