Java ConcurrentModificationException 深度剖析开发调试日志的解决方案
作者:熊猫钓鱼>_>
前言
在Java多线程编程中,ConcurrentModificationException是一个常见的异常,它不仅出现在多线程环境,也会在单线程环境中出现。本文将深入分析这个异常的产生原因、触发条件,并提供多种解决方案及其性能对比,帮助开发者在实际项目中做出最佳选择。
异常概述
ConcurrentModificationException是Java集合框架中的一个运行时异常,它在以下情况下会被抛出:
- 当一个线程正在迭代集合,而另一个线程同时修改了该集合的结构(添加、删除元素)
- 当在单线程环境中,使用迭代器遍历集合的同时,通过集合自身的方法修改集合结构
这个异常是Java集合框架的一种**快速失败(fail-fast)**机制,用于检测并发修改,防止程序在不确定状态下继续执行。
在我们的实际测试中,我们发现即使在单线程环境下,如果在遍历过程中直接修改集合,也会抛出此异常。例如:
List<String> fruits = new ArrayList<>();
fruits.add("香蕉");
fruits.add("西瓜");
try {
for (String fruit : fruits) {
if (fruit.equals("香蕉")) {
fruits.remove(fruit); // 这里会抛出ConcurrentModificationException
}
}
} catch (ConcurrentModificationException e) {
System.out.println("异常信息: " + e.getMessage());
}单线程环境下的异常分析
异常复现
在单线程环境下,以下代码会触发ConcurrentModificationException:
List<String> list = new ArrayList<>();
list.add("item1");
list.add("item2");
list.add("item3");
// 使用for-each循环(底层使用Iterator)
for (String item : list) {
if ("item2".equals(item)) {
list.remove(item); // 这里会抛出ConcurrentModificationException
}
}源码分析
为什么会抛出这个异常?让我们看看ArrayList的Iterator实现:
- 当创建Iterator时,会记录当前集合的
modCount值(修改计数器)到expectedModCount - 每次调用
next()方法时,会检查modCount是否等于expectedModCount - 如果不相等,说明集合在迭代过程中被修改,立即抛出
ConcurrentModificationException
关键源码(简化版):
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
public E next() {
checkForComodification();
// ...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}在我们的测试中,我们还发现不仅List集合会出现这个问题,Map集合同样存在类似问题:
Map<String, String> caches = new HashMap<>();
caches.put("user@getAge@123@v1", "30");
caches.put("user@getAddress@456@v1", "New York");
String sameKeyPart = "user@get";
try {
Iterator<String> keys = caches.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
System.out.println("当前键: " + key);
if (key.startsWith(sameKeyPart)) {
caches.remove(key); // 这里会抛出ConcurrentModificationException
}
}
} catch (ConcurrentModificationException e) {
System.out.println("捕获异常: " + e.getClass().getName());
}正确解决方法
- 使用Iterator的remove方法:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("item2".equals(item)) {
iterator.remove(); // 正确的方式
}
}在我们的测试代码中,我们验证了这种方法的有效性:
Map<String, String> caches = new HashMap<>();
caches.put("user@getName@123@v1", "John");
caches.put("user@getEmail@123@v1", "john@example.com");
String sameKeyPart = "user@get";
Iterator<String> keys = caches.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
if (key.startsWith(sameKeyPart)) {
keys.remove(); // 使用Iterator的remove方法
System.out.println("已删除: " + key);
}
}- 使用Java 8+ 的removeIf方法:
list.removeIf(item -> "item2".equals(item));
在我们的测试中,这种方法同样有效:
List<String> fruits = new ArrayList<>();
fruits.add("香蕉");
fruits.add("苹果");
fruits.add("橙子");
fruits.removeIf(fruit -> fruit.equals("香蕉"));
System.out.println("删除后: " + fruits);多线程环境下的异常分析
多线程环境下,即使使用了Iterator的remove方法,仍然可能发生ConcurrentModificationException,因为多个线程可能同时修改集合。
异常复现
List<String> list = new ArrayList<>();
// 初始化列表...
// 线程1:遍历列表
new Thread(() -> {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
try {
Thread.sleep(100); // 模拟耗时操作
String item = iterator.next(); // 可能抛出异常
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
// 线程2:修改列表
new Thread(() -> {
try {
Thread.sleep(50);
list.add("newItem"); // 修改集合结构
} catch (Exception e) {
e.printStackTrace();
}
}).start();在我们的实际测试中,我们创建了一个更完整的示例:
private static void demoMultiThreadWithArrayList() {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add("Item " + i);
}
// 创建一个线程用于遍历列表
Thread readerThread = new Thread(() -> {
try {
System.out.println("读取线程开始遍历");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
Thread.sleep(100); // 模拟处理时间
System.out.println("读取线程: " + item);
}
System.out.println("读取线程完成遍历");
} catch (ConcurrentModificationException e) {
System.out.println("读取线程捕获异常: " + e.getClass().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 创建一个线程用于修改列表
Thread writerThread = new Thread(() -> {
try {
Thread.sleep(300); // 等待读取线程开始
list.add("New Item"); // 添加新元素
System.out.println("修改线程添加了新元素");
Thread.sleep(100);
list.remove(0); // 删除元素
System.out.println("修改线程删除了元素");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
writerThread.start();
readerThread.start();
try {
writerThread.join();
readerThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}线程安全分析
在多线程环境下,ArrayList等非线程安全集合存在以下问题:
- 结构性修改的原子性:添加或删除元素不是原子操作
- 可见性问题:一个线程的修改对另一个线程不一定立即可见
- 一致性问题:迭代器可能看到集合的不一致状态
解决方案对比
| 解决方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Collections.synchronizedList | 读写频率相近 | 简单易用 | 性能较低,锁粒度大 |
| CopyOnWriteArrayList | 读多写少 | 读取无锁,性能高 | 写入性能差,内存占用高 |
| ConcurrentHashMap | 需要高并发Map | 分段锁,性能好 | 仅适用于Map |
| CopiedIterator(自定义) | 读写分离场景 | 避免长时间锁定 | 额外内存开销 |
| 快照技术 | 一次性读取后修改 | 简单直观 | 不适合大数据量 |
| Stream API | 函数式处理 | 代码简洁,可并行 | Java 8+才支持 |
在我们的测试中,我们对几种主要的解决方案进行了实际验证:
1. Collections.synchronizedList
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
// 需要注意的是,遍历时仍需要手动同步
synchronized (synchronizedList) {
Iterator<String> iterator = synchronizedList.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println("读取线程: " + item);
}
}2. CopyOnWriteArrayList
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
// 可以安全地在遍历过程中修改
for (String item : copyOnWriteList) {
System.out.println("当前元素: " + item);
copyOnWriteList.add("New Item"); // 不会抛出异常
}CopiedIterator实现与分析
CopiedIterator是一种自定义解决方案,它在创建迭代器时复制集合内容,从而避免并发修改异常。
实现代码
public static class CopiedIterator<E> implements Iterator<E> {
private Iterator<E> iterator = null;
public CopiedIterator(Iterator<E> itr) {
LinkedList<E> list = new LinkedList<>();
while(itr.hasNext()) {
list.add(itr.next());
}
this.iterator = list.iterator();
}
public boolean hasNext() {
return this.iterator.hasNext();
}
public void remove() {
throw new UnsupportedOperationException("这是一个只读迭代器");
}
public E next() {
return this.iterator.next();
}
}使用方式
List<String> list = new ArrayList<>();
// 初始化列表...
// 创建CopiedIterator
Iterator<String> safeIterator;
synchronized(list) {
safeIterator = new CopiedIterator<>(list.iterator());
}
// 安全遍历,不会抛出ConcurrentModificationException
while(safeIterator.hasNext()) {
String item = safeIterator.next();
// 处理元素...
}在我们的实际测试中,我们发现这种方案在特定场景下非常有效:
public static void perform() {
Iterator<String> iterator;
synchronized(list) {
iterator = new CopiedIterator<>(list.iterator());
}
System.out.println("获取到只读迭代器,开始遍历");
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println("遍历元素: " + item);
try {
Thread.sleep(100); // 模拟处理时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("遍历完成");
}优缺点分析
优点:
- 避免了长时间锁定集合
- 适用于任何实现了Iterator接口的集合
- 实现简单,容易理解
缺点:
- 额外的内存开销,尤其是对大型集合
- 只能提供集合的快照,无法反映后续修改
- 不支持修改操作(如remove)
在我们的性能测试中,我们发现对于包含10000个元素的列表,CopiedIterator的额外开销大约为10-15毫秒,这对于需要长时间处理的场景来说是可以接受的。
高级解决方案
1. ConcurrentHashMap
ConcurrentHashMap是一个高性能的线程安全Map实现,它使用分段锁技术提高并发性能。
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
// 可以安全地在遍历过程中修改
for (String key : concurrentMap.keySet()) {
concurrentMap.put("newKey", "newValue"); // 不会抛出异常
}在我们的测试中,我们验证了ConcurrentHashMap的线程安全性:
private static void demoConcurrentHashMap() {
Map<String, String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key1", "value1");
concurrentMap.put("key2", "value2");
// 测试ConcurrentHashMap
for (String key : concurrentMap.keySet()) {
if (key.equals("key2")) {
concurrentMap.put("key4", "value4"); // 不会抛出异常
System.out.println("添加了新键值对: key4=value4");
}
}
System.out.println("ConcurrentHashMap最终大小: " + concurrentMap.size());
}2. CopyOnWriteArrayList/Set
CopyOnWriteArrayList和CopyOnWriteArraySet在每次写操作时都会复制整个底层数组,非常适合读多写少的场景。
List<String> cowList = new CopyOnWriteArrayList<>();
// 可以安全地在遍历过程中修改
for (String item : cowList) {
cowList.add("newItem"); // 不会抛出异常
}我们的测试代码验证了这一点:
private static void demoCopyOnWriteArraySet() {
// 创建CopyOnWriteArraySet
Set<String> cowSet = new CopyOnWriteArraySet<>();
cowSet.add("item1");
cowSet.add("item2");
cowSet.add("item3");
System.out.println("\n尝试在遍历CopyOnWriteArraySet时修改:");
for (String item : cowSet) {
System.out.println("当前元素: " + item);
cowSet.add("item4"); // 不会抛出异常
}
System.out.println("CopyOnWriteArraySet内容: " + cowSet);
}3. 快照技术
快照技术是一种简单的解决方案,适用于一次性读取后修改的场景。
List<String> originalList = new ArrayList<>();
// 初始化列表...
// 创建快照
List<String> snapshot = new ArrayList<>(originalList);
// 遍历快照,修改原始列表
for (String item : snapshot) {
if (someCondition(item)) {
originalList.remove(item);
}
}我们在测试中也验证了这种技术:
private static void demoSnapshotTechnique() {
List<String> originalList = new ArrayList<>();
originalList.add("item1");
originalList.add("item2");
originalList.add("item3");
System.out.println("原始列表: " + originalList);
List<String> snapshot = new ArrayList<>(originalList);
System.out.println("遍历快照并修改原始列表:");
for (String item : snapshot) {
System.out.println("当前元素: " + item);
if (item.equals("item2")) {
originalList.remove(item);
}
}
System.out.println("修改后原始列表: " + originalList);
System.out.println("快照内容保持不变: " + snapshot);
}4. Stream API
Java 8引入的Stream API提供了一种函数式处理集合的方式,可以避免显式迭代。
List<String> result = list.stream()
.filter(item -> !item.equals("item2"))
.collect(Collectors.toList());
在我们的测试中,我们使用了Stream API的各种功能:
private static void demoStreamAPI() {
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("grape");
System.out.println("\n使用Stream API过滤元素:");
List<String> filteredList = list.stream()
.filter(item -> !item.equals("banana"))
.collect(Collectors.toList());
System.out.println("过滤后: " + filteredList);
System.out.println("\n使用Stream API转换元素:");
List<String> upperCaseList = list.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("转换后: " + upperCaseList);
}性能测试与对比
我们对不同解决方案进行了性能测试,以下是结果分析:
1. 遍历性能对比(10,000元素)
| 解决方案 | 平均耗时(ms) |
|---|---|
| 普通Iterator | 1-2 |
| CopiedIterator | 10-15 |
| CopyOnWriteArrayList | 1-2 |
| Collections.synchronizedList | 3-5 |
| Stream API (顺序) | 5-8 |
| Stream API (并行) | 2-4 |
在我们的性能测试代码中,我们进行了实际测量:
private static void performanceTest() {
// 准备大数据集
List<String> largeList = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
largeList.add("Item-" + i);
}
// 测试普通Iterator
long startTime = System.nanoTime();
Iterator<String> normalIterator = largeList.iterator();
int count = 0;
while (normalIterator.hasNext()) {
normalIterator.next();
count++;
}
long normalTime = System.nanoTime() - startTime;
// 测试CopiedIterator
startTime = System.nanoTime();
Iterator<String> copiedIterator = new CopiedIterator<>(largeList.iterator());
count = 0;
while (copiedIterator.hasNext()) {
copiedIterator.next();
count++;
}
long copiedTime = System.nanoTime() - startTime;
System.out.println("普通Iterator遍历时间: " + TimeUnit.NANOSECONDS.toMillis(normalTime) + " 毫秒");
System.out.println("CopiedIterator遍历时间: " + TimeUnit.NANOSECONDS.toMillis(copiedTime) + " 毫秒");
System.out.println("CopiedIterator额外开销: " + (copiedTime - normalTime) / 1000000.0 + " 毫秒");
}2. 修改性能对比(10,000元素,添加操作)
| 解决方案 | 平均耗时(ms) |
|---|---|
| ArrayList | 0.1-0.2 |
| CopyOnWriteArrayList | 50-100 |
| Collections.synchronizedList | 0.5-1 |
| ConcurrentHashMap (put) | 0.2-0.5 |
3. 内存占用对比
| 解决方案 | 相对内存占用 |
|---|---|
| ArrayList | 1x |
| CopiedIterator | 2x |
| CopyOnWriteArrayList (写操作时) | 2x |
| 快照技术 | 2x |
异常处理机制深入分析
fail-fast机制原理
Java集合框架中的fail-fast机制是一种错误检测机制,它能帮助开发者尽早发现程序中的并发修改问题。当多个线程对集合进行结构上的改变时,就可能产生fail-fast事件。
在ArrayList中,modCount变量记录了集合结构修改的次数。每次调用add、remove等修改结构的方法时,modCount都会增加。同时,Iterator在创建时会保存当前的modCount值作为expectedModCount。每次调用Iterator的next()方法时,都会检查modCount是否与expectedModCount相等,如果不相等则抛出ConcurrentModificationException。
// ArrayList中的add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// AbstractList中的ensureCapacityInternal方法
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
modCount++; // 修改计数器增加
ensureExplicitCapacity(minCapacity);
}异常传播与处理
在实际应用中,我们需要合理处理ConcurrentModificationException异常。以下是我们推荐的处理方式:
- 预防为主:使用线程安全的集合类或同步机制来避免异常的发生
- 捕获并记录:在无法避免异常的情况下,捕获异常并记录日志
- 优雅降级:提供备选方案,确保系统在异常情况下仍能正常运行
public class SafeListProcessor {
private List<String> dataList;
public SafeListProcessor(List<String> dataList) {
this.dataList = dataList;
}
public void processList() {
Iterator<String> iterator = null;
synchronized(dataList) {
iterator = new CopiedIterator<>(dataList.iterator());
}
try {
while (iterator.hasNext()) {
String item = iterator.next();
// 处理元素
processItem(item);
}
} catch (ConcurrentModificationException e) {
// 记录异常日志
System.err.println("检测到并发修改异常: " + e.getMessage());
// 可以选择重试或使用备选方案
handleConcurrentModification();
}
}
private void processItem(String item) {
// 处理单个元素
System.out.println("处理元素: " + item);
}
private void handleConcurrentModification() {
// 处理并发修改异常的备选方案
System.out.println("使用备选方案处理数据");
}
}实际应用建议
选择合适的解决方案
在实际项目中,我们需要根据具体场景选择合适的解决方案:
- 读多写少场景:
- 推荐使用CopyOnWriteArrayList/CopyOnWriteArraySet
- 适用于缓存、配置信息等场景
- 高并发读写场景:
- 推荐使用ConcurrentHashMap
- 适用于需要高并发访问的Map结构
- 需要长时间遍历的场景:
- 推荐使用CopiedIterator或快照技术
- 适用于需要对大量数据进行复杂处理的场景
- 简单过滤或转换场景:
- 推荐使用Stream API
- 代码简洁,可读性强
代码示例
以下是我们项目中实际使用的代码示例:
// 使用CopyOnWriteArrayList处理配置信息
public class ConfigManager {
private CopyOnWriteArrayList<ConfigItem> configItems = new CopyOnWriteArrayList<>();
public void addConfig(ConfigItem item) {
configItems.add(item);
}
public List<ConfigItem> getActiveConfigs() {
// 可以安全地遍历,即使其他线程正在修改
return configItems.stream()
.filter(ConfigItem::isActive)
.collect(Collectors.toList());
}
}
// 使用ConcurrentHashMap处理用户会话
public class SessionManager {
private ConcurrentHashMap<String, UserSession> sessions = new ConcurrentHashMap<>();
public void addSession(String sessionId, UserSession session) {
sessions.put(sessionId, session);
}
public void cleanupExpiredSessions() {
// 可以安全地遍历并修改
sessions.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
}
// 使用CopiedIterator处理长时间运行的任务
public class DataProcessor {
private List<DataItem> dataItems;
public void processLargeDataSet() {
Iterator<DataItem> iterator;
synchronized(dataItems) {
iterator = new CopiedIterator<>(dataItems.iterator());
}
// 长时间处理不会阻塞其他线程对dataItems的修改
while (iterator.hasNext()) {
DataItem item = iterator.next();
processComplexCalculation(item);
}
}
}性能优化建议
- 合理预估集合大小:
- 使用带初始容量的构造函数避免频繁扩容
- 例如:
new ArrayList<>(1000)而不是new ArrayList<>();
- 选择合适的数据结构:
- 频繁随机访问:ArrayList
- 频繁插入删除:LinkedList
- 需要排序:TreeSet/TreeMap
- 唯一性要求:HashSet/HashMap
- 减少锁竞争:
- 缩小同步块范围
- 使用读写锁分离读写操作
- 考虑使用无锁数据结构
最佳实践总结
单线程环境
- 避免在for-each循环中修改集合
- 使用Iterator的remove()方法
- 使用Java 8+的removeIf()、replaceAll()等方法
- 创建集合副本进行遍历
- 批量操作优于单个操作
- 使用addAll()、removeAll()等批量方法
- 使用Stream API进行批量处理
在我们的测试中,我们发现removeIf()方法特别适用于简单的过滤操作:
List<String> fruits = new ArrayList<>();
fruits.add("香蕉");
fruits.add("苹果");
fruits.add("橙子");
// 使用removeIf进行过滤
fruits.removeIf(fruit -> fruit.equals("香蕉"));
System.out.println("删除后: " + fruits);多线程环境
- 选择合适的线程安全集合
- 读多写少:CopyOnWriteArrayList/Set
- 读写频率相近:Collections.synchronizedList + 同步块
- 高并发Map:ConcurrentHashMap
- 避免长时间锁定集合
- 使用CopiedIterator或快照技术
- 缩小同步块范围
- 考虑使用并发工具类
- BlockingQueue系列
- ConcurrentSkipListMap/Set
在我们的多线程测试中,我们发现CopyOnWriteArrayList在读多写少的场景下表现优异:
private static void demoCopyOnWriteArrayList() {
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
copyOnWriteList.add("Item " + i);
}
Thread readerThread = new Thread(() -> {
System.out.println("读取线程开始遍历CopyOnWriteArrayList");
Iterator<String> iterator = copyOnWriteList.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println("读取线程: " + item);
try {
Thread.sleep(100); // 模拟处理时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("读取线程完成遍历");
});
Thread writerThread = new Thread(() -> {
try {
System.out.println("修改线程开始修改CopyOnWriteArrayList");
copyOnWriteList.add("New Item"); // 添加新元素
System.out.println("修改线程添加了新元素");
Thread.sleep(100);
copyOnWriteList.remove(0); // 删除元素
System.out.println("修改线程删除了元素");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
readerThread.start();
writerThread.start();
try {
readerThread.join();
writerThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("最终列表大小: " + copyOnWriteList.size());
}性能优化
- 根据访问模式选择集合
- 随机访问多:ArrayList
- 插入删除多:LinkedList
- 唯一性要求:HashSet/TreeSet
- 预估集合大小
- 使用构造函数指定初始容量
- 避免频繁扩容
- 减少不必要的复制
- 谨慎使用CopyOnWrite集合
- 优化CopiedIterator实现
在我们的测试中,我们发现对于大数据集,Stream API的并行处理能力非常强大:
private static void demoParallelStream() {
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
numbers.add(i);
}
System.out.println("\n使用并行流处理大量数据:");
long startTime = System.nanoTime();
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
long sequentialTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
int parallelSum = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();
long parallelTime = System.nanoTime() - startTime;
System.out.println("顺序流处理时间: " + TimeUnit.NANOSECONDS.toMicros(sequentialTime) + " 微秒");
System.out.println("并行流处理时间: " + TimeUnit.NANOSECONDS.toMicros(parallelTime) + " 微秒");
System.out.println("结果验证: " + (sum == parallelSum ? "正确" : "错误"));
}到此这篇关于Java ConcurrentModificationException 深度剖析开发调试日志的解决方案的文章就介绍到这了,更多相关Java ConcurrentModificationException调试日志内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- Java 报错 java.util.ConcurrentModificationException: null 的原因及解决方案
- Java ConcurrentModificationException异常解决案例详解
- 详解Java删除Map中元素java.util.ConcurrentModificationException”异常解决
- Java源码解析ArrayList及ConcurrentModificationException
- 出现java.util.ConcurrentModificationException 问题及解决办法
- java.util.ConcurrentModificationException 解决方法
- java 集合并发操作出现的异常ConcurrentModificationException
