若key对应的value已经存在,多个线程并发获取该key对应value时。会存在性能问题。
bug详见:https://bugs.openjdk.java.net/browse/JDK-8161372
1. 源码分析
源码分析:
ConcurrentHashMap提供的computeIfAbsent源码分析
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//hash没有初始化时
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) { //key未维护value时
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val, null);
} finally {
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED) //hash迁移
tab = helpTransfer(tab, f);
else { //value已经存在时,但是获取value时,有sync关键字
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek; V ev;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
added = true;
pred.next = new Node<K,V>(h, key, val, null);
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(h, key, null)) != null)
val = p.val;
else if ((val = mappingFunction.apply(key)) != null) {
added = true;
t.putTreeVal(h, key, val);
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (!added)
return val;
break;
}
}
}
if (val != null)
addCount(1L, binCount);
return val;
}
由上面的源码可知:当value存在时,每次获取value都有sync关键字。
2. 复现
测试代码
@Slf4j
public class Main {
static final int MAP_SIZE = 20;
static final int THREADS = 20;
static final ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
static {
for (int i = 0; i < MAP_SIZE; i++) map.put(i, i);
}
static class TestThread extends Thread {
public void run() {
int i = 0;
int result = 0;
while (result < Integer.MAX_VALUE) {
i = (i + 1) % MAP_SIZE;
result += map.computeIfAbsent(i, (key) -> key + key);
}
}
}
public static void main(String[] v) throws InterruptedException {
ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < THREADS; i++) {
TestThread t = new TestThread();
threads.add(t);
t.start();
}
threads.get(0).join();
}
}
使用jps
命令查询pid,使用jstack pid
命令查询线程堆栈信息。
"Thread-4" #15 prio=5 os_prio=31 tid=0x00007fb83e92e800 nid=0xa403 waiting for monitor entry [0x000070000900a000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1674)
- waiting to lock <0x000000076b4c5470> (a java.util.concurrent.ConcurrentHashMap$Node)
at com.tellme.test.Main$TestThread.run(Main.java:29)
可以看到,当大并发获取value时,会在ConcurrentHashMap.java:1674
行被阻塞。而这一行就是synchronized (f)
代码。
3. 优化
优化方案:直接先通过get方法获取value,获取不到时,才使用computeIfAbsent
方法去维护缓存。
public static <K, V> V computeIfAbsent(Map<K, V> concurrentHashMap, K key, Function<? super K, ? extends V> mappingFunction) {
V v = concurrentHashMap.get(key);
if (v != null) {
return v;
}
return concurrentHashMap.computeIfAbsent(key, mappingFunction);
}