一个ConcurrentModificationException的原因分析

在迭代List时,如果不通过iterator去修改list,那么将得到ConcurrentModificationException。

所以一般自己写的代码都会尽力避免这样的事情。但如果迭代和修改被分布在不同类的方法里,那么问题就很隐蔽了。

有一个同事写了一段这样的代码:

//代码段1
List<List<A>> slicedList = ListUtil.sliceList(someList,size);
someList.clear();
for(List<A> subList: slicedList ){
//迭代subList进行其他处理
}

拿到一个大的List,然后按照给定的大小拆分成多个小的list,再清空原有的list,然后循环多个小的list。

这货自己能产生ConcurrentModificationException吗?普通的实现下,肯定不会有问题。


但是悲剧还是出现了。

//代码段2
public static <T> List<List<T>> sliceList(final List<T> list, int batchSize) {
List<List<T>> result = new ArrayList<List<T>>();
if (CollectionUtils.isEmpty(list) || 0 >= batchSize) {
return result;
}

final int n = (list.size() + batchSize - 1) / batchSize;
for (int i = 0; i < n; i++) {
result.add(list.subList(i * batchSize, Math.min((1 + i) * batchSize, list.size())));//这里
}

return result;
}

sliceList的实现导致代码段1出现ConcurrentModificationException。

原因:sliceList方法调用了list.subList,[b][color=red]subList其实不过是原有somelist的视图[/color][/b],但是subList保存了somelist的modCount(修改操作的数量)。

在代码段1中,

sliceList方法之后clear方法被调用了,导致someList的modCount发生了变化。

在随后的“迭代subList进行其他处理”中,subList将被迭代,而subList不过是somelist的视图,它持有someList的modcount。


迭代subList时,迭代器的next方法将被调用,而next的第一步就是checkForComodification(),它检查someList的modCount是否与自身持有的modCount,很显然因为视图中记录了modCount之后,代码段1中clear操作已经将modCount增加了,所以不相等。

再来捋一捋:

1、sliceList方法先执行,其中的subList方法返回了someList的视图,视图记录了someList的modCount。具体参考SubList的构造方法。

2、clear方法被执行,modCount加1;

3、“迭代subList进行其他处理”开始执行,开始遍历subList方法返回的视图,该视图的next方法把自己持有的宝贝modCount与someList的modCount进行了比较。发现了不一致,抛出异常。具体参考AbstractList中Itr的next方法


综上,sliceList的实现是一个悲剧。它的实现埋下了一个隐秘的大坑。其实,在代码段2中只要subList替换成一个new一个list即可避免这样的问题。具体实现很简单。

List的具体实现可参考:AbstractList的方法subList和内部类SubList。
### Java `foreach` 循环中的 `ConcurrentModificationException` #### 原因分析 当使用增强型for循环(`foreach`)遍历集合时,如果在迭代过程中修改了该集合(除了通过迭代器自身的移除操作),则会抛出 `ConcurrentModificationException`。此异常是为了防止并发修改带来的不确定性[^1]。 具体来说,在给定的例子中: ```java at java.util.ArrayList.forEach(ArrayList.java:1252) ``` 表明是在调用 `ArrayList` 的 `forEach` 方法期间发生的异常。这通常意味着在同一时间有另一个线程正在修改这个列表的内容,或者是同一个线程内存在不恰当的操作尝试改变被迭代对象的状态[^2]。 #### 解决方案 为了处理这种情况并避免 `ConcurrentModificationException`,可以采取以下几种方式之一来解决问题: ##### 使用 Iterator 进行安全删除 利用 `Iterator.remove()` 来代替直接对集合进行增删改查动作是最常见的做法。这种方式允许在一个元素被访问之后立即对其进行删除而不引发异常。 ```java List<String> list = new ArrayList<>(); // ... 添加一些字符串到list... Iterator<String> it = list.iterator(); while (it.hasNext()) { String item = it.next(); if (someCondition(item)) { // 如果满足某些条件,则执行删除 it.remove(); // 安全地移除当前项 } } ``` ##### 利用 CopyOnWriteArrayList 替代普通 List 实现类 对于多线程环境下的频繁读取场景,考虑采用 `CopyOnWriteArrayList` 类作为替代品。这类容器会在每次写入时创建一个新的副本,从而使得迭代过程不会受到其他地方更改的影响。 ```java import java.util.concurrent.CopyOnWriteArrayList; public class SafeIterationExample { public static void main(String[] args) { CopyOnWriteArrayList<Integer> numbers = new CopyOnWriteArrayList<>(Arrays.asList(1, 2, 3)); for(Integer num : numbers){ System.out.println(num); numbers.add(num * 2); // 不会引起 ConcurrentModificationException } System.out.println(numbers.toString()); } } ``` ##### 同步控制机制 确保同一时刻只有一个线程能够访问共享资源也是一种有效的策略。可以通过同步代码块或者锁机制实现这一点。 ```java synchronized(list) { for (String element : list) { // 对element做些事情 } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值