ConcurrentHashMap抛出IllegalStateException: Recursive Update问题排查
问题分析与解决方案
核心结论
这是Java 11版本中ConcurrentHashMap.computeIfAbsent方法的并发场景误判问题,并非你的使用疏漏。
异常原因解析
你对源码的分析完全正确:
- Java 11的
ConcurrentHashMap在computeIfAbsent逻辑中,当映射函数(generateImplementation)执行耗时较长时,其他线程可能已经完成当前key的节点插入操作。 - 当前线程执行到
pred.next != null检查时,会误将这种合法的并发插入判定为递归更新,抛出IllegalStateException: Recursive update,但堆栈中并没有递归调用的痕迹。 - Java 8的
computeIfAbsent实现没有这个检查逻辑,因此你的代码在Java 8下正常,仅在Java 11出现问题。
同时可以确认你的使用完全合规:映射函数是无副作用的纯函数,没有违反“映射函数不得在计算期间修改此映射”的API要求。
可行解决方案
1. 手动实现双重检查锁定(Double-Checked Locking)
绕过computeIfAbsent的内部检查,手动控制并发缓存逻辑:
public Class<? extends _Artifact_> getImplementationOf(Class<_Artifact_> type, boolean defaultPackage) { // 第一次非同步检查 Class<? extends _Artifact_> clazz = implementationClassCache.get(type); if (clazz == null) { // 针对缓存加锁,避免重复生成 synchronized (implementationClassCache) { // 第二次同步检查 clazz = implementationClassCache.get(type); if (clazz == null) { clazz = generateImplementation(type, defaultPackage); implementationClassCache.put(type, clazz); } } } return clazz; }
注:如果缓存key数量极大,也可以针对单个key的哈希值加锁,减少锁竞争,但实现复杂度会更高。
2. 升级Java版本
该误判问题在后续Java版本(如Java 17)中已被修复,调整了computeIfAbsent的内部检查逻辑,避免将合法的并发插入判定为递归更新。
3. 替换为Guava LoadingCache
使用Guava的LoadingCache,它的并发加载逻辑经过充分优化,天然支持这种耗时的缓存生成场景:
// 初始化缓存 private final LoadingCache<Class<_Artifact_>, Class<? extends _Artifact_>> implementationClassCache = CacheBuilder.newBuilder() .concurrencyLevel(Runtime.getRuntime().availableProcessors()) .build(new CacheLoader<>() { @Override public Class<? extends _Artifact_> load(Class<_Artifact_> type) throws Exception { return generateImplementation(type, false); // 根据需求调整defaultPackage参数 } }); // 获取实现类方法 public Class<? extends _Artifact_> getImplementationOf(Class<_Artifact_> type, boolean defaultPackage) { // 若defaultPackage为可变参数,需将key改为包含该参数的组合类(如Pair) try { return implementationClassCache.get(type); } catch (ExecutionException e) { throw new RuntimeException(e.getCause()); } }
内容的提问来源于stack exchange,提问作者raner
相关产品推荐
相关产品推荐

