这个double check有问题吗?
引用:
1.http://blog.chinaunix.net/uid-28811518-id-5752680.html
2.https://www.cnblogs.com/yangchen-geek/p/15469076.html
1.问题
背景:某个方法需要动态生成对象,这些对象基于名称单例,为了较为效率的生成和获取,因此引入ConcurrentHashMap去先生成某个对象的锁,然后再获取该锁,再去初始化对象。但是生成的锁无法使用volatile修饰。代码类似如下,请问代码里面的double check有问题吗?
import org.apache.commons.lang.StringUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class DynamicFeignClientBuilderV3 {
/**
* 本地缓存
*/
private Map<String, Object> cache = new ConcurrentHashMap<>();
/**
* 锁映射
*/
private Map<String, Object> lockMap = new ConcurrentHashMap<>();
public <T, M> T getFeignClientUseModel(final Class<T> type, String targetServiceName, final Class<M> modelClass) {
// 尝试从本地缓存获取
T clientProxy = getFromLocalCache(targetServiceName, type);
if (clientProxy != null) {
return clientProxy;
}
String lockName = targetServiceName + "Lock";
// 尝试生成一个锁(cas,并发下保证只有一个线程能put成功)
lockMap.putIfAbsent(lockName, new Object());
Object lockOfTargetServiceName = lockMap.get(lockName);
clientProxy = getFromLocalCache(targetServiceName, type);
if (clientProxy != null) {
return clientProxy;
}
synchronized (lockOfTargetServiceName) {
clientProxy = getFromLocalCache(targetServiceName, type);
if (clientProxy != null) {
return clientProxy;
}
Object target = new Object();
cache.put(targetServiceName, target);
return (T)target;
}
}
private <T> T getFromLocalCache(String serviceName, Class<T> clazz) {
if (StringUtils.isBlank(serviceName) || clazz == null) {
throw new IllegalArgumentException("传入的参数中存在空值");
}
Object objectTemp = cache.get(serviceName);
if (objectTemp == null) {
return null;
}
if (clazz.isInstance(objectTemp)) {
return (T) objectTemp;
}
throw new IllegalArgumentException(String.format("通过服务名称【%s】,获取到的实例类型为【%s】,而非【%s】", serviceName, objectTemp.getClass().getName(), clazz.getName()));
}
}
2.笔者看法
笔者认为,double check是一种较为效率的解决单例重复初始化的方式,但是double check的引入带来了一些问题,即以下
XXX xxx = new XXX()
可以拆分成
(1)申请内存区域
(2)内存区域初始化(赋值之类的)
(3)引用指向该内存区域
但是由于JMM(java memory model)会对指令重排序,所以导致了另外的线程第一次check的时候,由于引用已经指向了某内存区域,但是内存初始化还未完成,进而导致另外的线程使用内存区域里面的值时报错。而引入volatile可以避免指令重排序,即解决了上面的情况。
但是这种情况明显不能应用于笔者提供的代码,笔者是这么认为的。首先double check判断存不存在的依据是Map是否包含这个key,而由于我加了synchronized和使用了ConcurrentHashMap,能保证只能有一个线程put进去Map,而put的操作只是将某个引用指向某个内存区域,并没有一个初始化的过程,因此我判断我这个double check不存在问题