1.http://blog.chinaunix.net/uid-28811518-id-5752680.html
2.https://www.cnblogs.com/yangchen-geek/p/15469076.html
背景:某个方法需要动态生成对象,这些对象基于名称单例,为了较为效率的生成和获取,因此引入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()));
}
}
笔者认为,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不存在问题
在给定的代码中,存在一个双重检查锁定(double-checked locking)的机制。具体 体现在以下代码段:
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;
}
在双重检查锁定中,首先在没有锁的情况下检查对象是否存在,然后再在同步块内进行检查和创建。这种机制旨在减少同步块的开销,提高性能。
然而,这段代码存在一个经典的线程安全问题,即指令重排序问题。在某些情况下,可能会导致其他线程获取到一个未完全初始化的对象。
要解决这个问题,需要在 cache
和 lockMap
变量声明时使用 volatile
关键字来确保可见性和禁止指令重排序。修改后的代码如下所示:
private volatile Map<String, Object> cache = new ConcurrentHashMap<>();
private volatile Map<String, Object> lockMap = new ConcurrentHashMap<>();
使用 volatile
关键字修饰的变量在多线程环境下具有可见性,任何对该变量的修改都会立即被其他线程所感知,从而避免了指令重排序带来的问题。
请注意,双重检查锁定在某些情况下可能不是必要的,具体取决于代码的上下文和要求。在某些情况下,使用简单的同步块即可满足线程安全的需求。因此,需要根据具体情况评估是否需要使用双重检查锁定。