你见过 ConcurrentHashMap 使用不当报错 java.lang.IllegalStateException: Recursive update 吗?

本文探讨了在Java中遇到的`java.util.ConcurrentModificationException`异常,特别是在ShardingSphere项目中出现的`java.lang.IllegalStateException:Recursiveupdate`问题。文章详细介绍了问题背景,分析了`ConcurrentHashMap`的源码,指出递归更新导致的问题,并通过对比HashMap的行为解释了为何`ConcurrentModificationException`会出现。最后,文章提出了问题的修复策略,避免在使用`ConcurrentHashMap`的`computeIfAbsent`时发生递归调用。

了解 Java 的同学,也许会见过一个异常 java.util.ConcurrentModificationException,这个一般在迭代器访问 Collection、Map 等数据结构过程中,修改了数据结构中的元素导致。
前段时间,ShardingSphere 遇到一个偶发报错的问题:java.lang.IllegalStateException: Recursive update。这个问题其实和并发修改问题有相似之处。本文主要记录该问题的排查与分析过程。

问题背景

相关 issue java.lang.IllegalStateException: Recursive update caused by #24251

Apache ShardingSphere 在 Proxy 模块重构后的一段时间里,频繁发生偶发报错 java.lang.IllegalStateException: Recursive update

引发该问题的重构 PR:https://github.com/apache/shardingsphere/pull/24251

java.util.ServiceConfigurationError: org.apache.shardingsphere.proxy.backend.mysql.handler.admin.MySQLSessionVariableHandler: Provider org.apache.shardingsphere.proxy.backend.mysql.handler.admin.MySQLDefaultSessionVariableHandler could not be instantiated

	at java.base/java.util.ServiceLoader.fail(ServiceLoader.java:586)
	at java.base/java.util.ServiceLoader$ProviderImpl.newInstance(ServiceLoader.java:813)
	at java.base/java.util.ServiceLoader$ProviderImpl.get(ServiceLoader.java:729)
	at java.base/java.util.ServiceLoader$3.next(ServiceLoader.java:1403)
	at org.apache.shardingsphere.infra.util.spi.ShardingSphereServiceLoader.load(ShardingSphereServiceLoader.java:56)
	at org.apache.shardingsphere.infra.util.spi.ShardingSphereServiceLoader.<init>(ShardingSphereServiceLoader.java:46)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
	at org.apache.shardingsphere.infra.util.spi.ShardingSphereServiceLoader.getServiceInstances(ShardingSphereServiceLoader.java:73)
	at org.apache.shardingsphere.infra.util.spi.type.typed.TypedSPILoader.findService(TypedSPILoader.java:71)
	at org.apache.shardingsphere.infra.util.spi.type.typed.TypedSPILoader.getService(TypedSPILoader.java:126)
	at org.apache.shardingsphere.infra.util.spi.type.typed.TypedSPILoader.getService(TypedSPILoader.java:113)
	at org.apache.shardingsphere.proxy.backend.mysql.handler.admin.MySQLSetVariableAdminExecutor.lambda$execute$0(MySQLSetVariableAdminExecutor.java:56)
	at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:180)
	at java.base/java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.base/java.util.HashMap$KeySpliterator.forEachRemaining(HashMap.java:1715)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
	at org.apache.shardingsphere.proxy.backend.mysql.handler.admin.MySQLSetVariableAdminExecutor.execute(MySQLSetVariableAdminExecutor.java:56)
	at org.apache.shardingsphere.proxy.backend.mysql.handler.admin.executor.MySQLSetVariableAdminExecutorTest.assertExecute(MySQLSetVariableAdminExecutorTest.java:66)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:578)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
	... 省略 JUnit 调用栈
Caused by: java.lang.IllegalStateException: Recursive update
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1763)
	at org.apache.shardingsphere.infra.util.spi.ShardingSphereServiceLoader.getServiceInstances(ShardingSphereServiceLoader.java:73)
	at org.apache.shardingsphere.infra.util.spi.type.typed.TypedSPILoader.findService(TypedSPILoader.java:71)
	at org.apache.shardingsphere.infra.util.spi.type.typed.TypedSPILoader.findService(TypedSPILoader.java:55)
	at org.apache.shardingsphere.proxy.backend.handler.admin.executor.DefaultSessionVariableHandler.<init>(DefaultSessionVariableHandler.java:36)
	at org.apache.shardingsphere.proxy.backend.mysql.handler.admin.MySQLDefaultSessionVariableHandler.<init>(MySQLDefaultSessionVariableHandler.java:28)
	at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:67)
	at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484)
	at java.base/java.util.ServiceLoader$ProviderImpl.newInstance(ServiceLoader.java:789)
	... 88 more

抛异常的是 ConcurrentHashMap 实例是以下的 LOADERS</

<think>好的,我现在需要帮助用户解决Java中使用Collectors.toMap时出现的Duplicate key错误。首先,我得仔细分析用户提供的代码和引用内容,理解问题所在。 用户的问题集中在处理List转Map时出现的重复键异常。根据引用中的代码示例,比如引用[2]和引用[3],问题通常发生在Stream的collect操作中,当有多个元素具有相同的键时,默认的toMap方法会抛出异常,因为它不知道如何处理重复的键。 首先,我需要解释代码的功能。比如,用户可能有一个包含对象的列表,他们试图将这些对象按照某个属性(如name)转换为Map,其中键是对象的属性,值是对象本身或其他属性。如果列表中有两个或多个对象具有相同的name属性,那么Collectors.toMap默认会使用throwingMerger,这时候遇到重复键就会抛出异常。 接下来,解决这个问题的方法。根据引用[3]中的例子,用户可能需要处理重复键的情况,比如选择保留第一个或最后一个出现的键,或者合并值。这时,应该建议在Collectors.toMap方法中传入一个合并函数,例如使用(v1, v2) -> v1来保留第一个,或者(v1, v2) -> v2保留最后一个。此外,用户可能需要检查数据源是否有重复的键,或者是否需要合并逻辑。 另外,用户可能在转换Map时没有意识到键可能重复,所以需要提醒他们在使用toMap时总是提供合并策略,即使他们预期没有重复。同时,可以建议使用groupingBy来处理可能需要多个值的情况,或者使用其他数据结构。 最后,结合用户提供的引用,尤其是引用[2]和[3]中的代码示例,我需要给出具体的解决方案,包括修改后的代码示例,并解释每个步骤的作用,确保用户能够理解如何应用这些解决方案到他们的实际代码中。</think>### 代码逻辑分析 提供的Java代码通过`Collectors.toMap`将对象列表转换为Map时,**当出现重复键时会触发`IllegalStateException: Duplicate key`异常**[^2][^3]。核心问题在于转换过程中存在多个元素具有相同的键值,而默认的`Collectors.toMap`方法不允许键重复。 以引用[3]的代码为例: ```java Map<String, Integer> studentMaps = listStudents().stream() .collect(HashMap::new, (m, v)-> m.put(v.getName(), v.getId()), HashMap::putAll); ``` 这里尝试将`Student`列表转换为`<学生姓名, 学生ID>`的Map。当两个学生姓名相同时(如"互联网全栈开发"和同名学生),就会因键重复导致异常。 ### 错误原因详解 1. **键冲突机制** `Collectors.toMap`默认使用`throwingMerger`合并函数,当检测到重复键时会立即抛出异常[^2] 2. **数据质量问题** 输入列表中存在多个具有相同键值的元素(如同名学生),但未定义合并策略 ### 解决方案 #### 方法一:显式定义合并策略(推荐) 在`Collectors.toMap`中增加第三个参数定义重复键处理逻辑: ```java // 保留第一个出现的键值 Map<String, Integer> studentMaps = listStudents().stream() .collect(Collectors.toMap( Student::getName, Student::getId, (existingValue, newValue) -> existingValue // 冲突时保留现有值 )); // 或者保留最后出现的键值 Map<String, Integer> studentMaps = listStudents().stream() .collect(Collectors.toMap( Student::getName, Student::getId, (existingValue, newValue) -> newValue // 冲突时覆盖为新值 )); ``` #### 方法二:使用groupingBy处理多值关联 当需要保留所有关联值时: ```java Map<String, List<Integer>> studentMaps = listStudents().stream() .collect(Collectors.groupingBy( Student::getName, Collectors.mapping(Student::getId, Collectors.toList()) )); ``` #### 方法三:数据预处理去重 在转换前过滤重复键: ```java Map<String, Integer> studentMaps = listStudents().stream() .filter(distinctByKey(Student::getName)) .collect(Collectors.toMap(Student::getName, Student::getId)); // 自定义去重过滤器 public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) { Map<Object, Boolean> seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } ``` ### 最佳实践建议 1. **强制定义合并策略**:即使当前数据无重复,也应始终显式声明合并函数以避免潜在异常 2. **数据质量验证**:转换前通过`System.out.println`输出数据或使用调试工具检查键值重复情况 3. **日志记录增强**:如引用[4]所示,在关键操作处添加try-catch块和日志记录
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wuweijie@apache.org

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值