为何ConcurrentHashMap的computeIfAbsent在JDK8无限循环,JDK17抛异常?
ConcurrentHashMap在JDK8与JDK17中的异常行为解析
问题代码示例
示例1:bad() 方法
public void bad() { final ConcurrentMap<String, Integer> chm = new ConcurrentHashMap<>(); final String key = "1"; chm.computeIfAbsent(key, __ -> { chm.remove(key); return 1; }); }
示例2:bad2() 方法
public void bad2() { final ConcurrentMap<String, Integer> chm = new ConcurrentHashMap<>(); final String key = "1"; Thread worker = new Thread(() -> chm.remove("1")); chm.computeIfAbsent(key, __ -> { worker.start(); try { worker.join(); } catch (InterruptedException e) { e.printStackTrace(); } return 1; }); }
JDK8中无限循环的根源
ConcurrentHashMap的computeIfAbsent执行时,会先锁定目标key对应的哈希桶。在lambda里调用chm.remove(key)时,当前线程已经持有该桶的锁,但JDK8的实现没有锁重入处理逻辑,反而会进入自旋等待锁释放:
- 对
bad()方法:lambda里的remove操作等待computeIfAbsent释放锁,而computeIfAbsent要等lambda执行完才释放锁,形成自死锁式的无限循环。 - 对
bad2()方法:worker线程调用remove时,需要获取同一哈希桶的锁,但主线程在computeIfAbsent里拿着锁,还在等worker线程结束;worker线程又卡在等主线程释放锁,双向等待导致线程间死锁,进而无限循环。
JDK17中抛出异常的原因
JDK17给ConcurrentHashMap的computeIfAbsent加了递归更新检测机制。当发现当前线程持有锁的情况下,又对同一key执行修改操作(比如这里的remove),会直接抛出IllegalStateException: Recursive update——目的就是提前阻断JDK8里那种无限循环的死锁场景,让开发者能立刻定位到代码里的逻辑错误。
内容的提问来源于stack exchange,提问作者ECPerth
相关产品推荐
相关产品推荐

