浅析Java对集合进行操作时报java.util.ConcurrentModificationException并发修改异常问题:产生原因、单线程/多线程环境解决、CopyOnWriteArrayList线程安全的ArrayList、fail-fast快速失败机制防止多线程修改集合造成并发问题
一、项目实例问题背景
本身对 java 并不熟,听同事说的,所以研究了解一下这个问题,看了一下产生问题的代码长这样
// 筛选出users中email不在memberListAll的email的成员
for (WorkWxUserInfoVO workWxUserInfoVO : memberListAll){
if (emailList.contains(workWxUserInfoVO.getEmail())) {
memberListAll.remove(workWxUserInfoVO);
}
workWxUserInfoVO.setUserid(null);
}
return memberListAll;
遍历 memberListAll 的同时 去删除 memberListAll 里的元素,目测感觉肯定是有问题的。深入原因见下文介绍。然后这样改一下就可以了,也就是重新声明一个 memberList 去承接,不要修改原先的 memberListAll 就行了。
List<WorkWxUserInfoVO> memberList = new ArrayList<>();
for (WorkWxUserInfoVO workWxUserInfoVO : memberListAll){
if (!emailList.contains(workWxUserInfoVO.getEmail())) {
workWxUserInfoVO.setUserid(null);
memberList.add(workWxUserInfoVO);
}
}
return memberList;
二、异常产生
当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException
的异常。
import java.util.Iterator;
import java.util.List;
public class AddRemoveListElement {
public static void main(String args[]) {
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
for (String s : list) {
if (s.equals("B")) {
list.remove(s);
}
}
//foreach循环等效于迭代器
/*Iterator<String> iterator=list.iterator();
while(iterator.hasNext()){
String s=iterator.next();
if (s.equals("B")) {
list.remove(s);
}
}*/
}
}
三、异常原因
ArrayList的父类AbstarctList中有一个域 modCount
,每次对集合进行修改(增添元素,删除元素……)时都会 modCount++
,而 foreach 的背后实现原理其实就是Iterator(关于Iterator可以看Java Design Pattern: Iterator),等同于注释部分代码。在这里,迭代ArrayList的Iterator中有一个变量 expectedModCount
,该变量会初始化和 modCount
相等,但如果接下来如果集合进行修改 modCount
改变,就会造成 expectedModCount!=modCount
,此时就会抛出java.util.ConcurrentModificationException异常。过程如下图:
我们再来根据源码详细的走一遍这个过程
/*
*AbstarctList的内部类,用于迭代
*/
private class Itr implements Iterator<E> {
int cursor = 0; //将要访问的元素的索引
int lastRet = -1; //上一个访问元素的索引
int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)
//判断是否还有下一个元素
public boolean hasNext() {
return cursor != size();
}
//取出下一个元素
public E next() {
checkForComodification(); //关键的一行代码,判断expectedModCount和modCount是否相等
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet == -1)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
根据代码可知,每次迭代list时,会初始化Itr的三个成员变量:cursor
、lastRe、
expectedModCount = modCount;
接着调用hasNext()
循环判断访问元素的下标是否到达末尾。如果没有,调用next()
方法,取出元素。
而最上面测试代码出现异常的原因在于,next()
方法调用checkForComodification()
时,发现了modCount != expectedModCount
public boolean add(E paramE) {
ensureCapacityInternal(this.size + 1);
/** 省略此处代码 */
}
private void ensureCapacityInternal(int paramInt) {
if (this.elementData == EMPTY_ELEMENTDATA)
paramInt = Math.max(10, paramInt);
ensureExplicitCapacity(paramInt);
}
private void ensureExplicitCapacity(int paramInt) {
this.modCount += 1; //修改modCount
/** 省略此处代码 */
}
public boolean remove(Object paramObject) {
int i;
if (paramObject == null)
for (i = 0; i < this.size; ++i) {
if (this.elementData[i] != null)
continue;
fastRemove(i);
return true;
}
else
for (i = 0; i < this.size; ++i) {
if (!(paramObject.equals(this.elementData[i])))
continue;
fastRemove(i);
return true;
}
return false;
}
private void fastRemove(int paramInt) {
this.modCount += 1; //修改modCount
/** 省略此处代码 */
}
public void clear() {
this.modCount += 1; //修改modCount
/** 省略此处代码 */
}
从上面的代码可以看出,ArrayList的add、remove、clear方法都会造成modCount的改变。迭代过程中如果调用这些方法就会造成modCount的增加,使迭代类中expectedModCount和modCount不相等。
四、异常的解决
现在我们已经基本了解了异常的发生原因了。接下来我们来解决它。我很任性,我就是想在迭代集合时删除集合的元素,怎么办?
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
String str = iter.next();
if( str.equals("B") ){
iter.remove();
}
}
细心的朋友会发现 Itr 中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了expectedModCount = modCount;
保证了不会抛出java.util.ConcurrentModificationException异常。
但是,这个办法的有两个弊端
(1)只能进行remove操作,add、clear等Itr中没有。
(2)而且只适用单线程环境。
2、多线程环境 —— 需要使用 CopyOnWriteArrayList
在多线程环境下,我们再次试验下上面的代码
public class Test2 {
static List<String> list = new ArrayList<String>();
public static void main(String[] args) {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
new Thread() {
public void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(Thread.currentThread().getName() + ":"
+ iterator.next());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}.start();
new Thread() {
public synchronized void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(Thread.currentThread().getName() + ":"
+ element);
if (element.equals("c")) {
iterator.remove();
}
}
};
}.start();
}
}
异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。
此时有两个办法:
(1)迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
(2)采用 CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作
static List<String> list = new CopyOnWriteArrayList<String>();
public class Test2 {
static List<String> list = new CopyOnWriteArrayList<String>();
public static void main(String[] args) {
list.add("a");
list.add("b");
list.add("c");
list.add("d");
new Thread() {
public void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(Thread.currentThread().getName()
+ ":" + iterator.next());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
}.start();
new Thread() {
public synchronized void run() {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(Thread.currentThread().getName()
+ ":" + element);
if (element.equals("c")) {
list.remove(element);
}
}
};
}.start();
}
}
五、深入理解异常—fail-fast机制
到这里,我们似乎已经理解完这个异常的产生缘由了。但是,仔细思考,还是会有几点疑惑:
1、既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
2、ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?
我们来看看源码中 modCount 的注解。
/**
* The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.
*
* <p>This field is used by the iterator and list iterator implementation
* returned by the {@code iterator} and {@code listIterator} methods.
* If the value of this field changes unexpectedly, the iterator (or list
* iterator) will throw a {@code ConcurrentModificationException} in
* response to the {@code next}, {@code remove}, {@code previous},
* {@code set} or {@code add} operations. This provides
* <i>fail-fast</i> behavior, rather than non-deterministic behavior in
* the face of concurrent modification during iteration.
*
* <p><b>Use of this field by subclasses is optional.</b> If a subclass
* wishes to provide fail-fast iterators (and list iterators), then it
* merely has to increment this field in its {@code add(int, E)} and
* {@code remove(int)} methods (and any other methods that it overrides
* that result in structural modifications to the list). A single call to
* {@code add(int, E)} or {@code remove(int)} must add no more than
* one to this field, or the iterators (and list iterators) will throw
* bogus {@code ConcurrentModificationExceptions}. If an implementation
* does not wish to provide fail-fast iterators, this field may be
* ignored.
*/
protected transient int modCount = 0;
我们注意到,注解中频繁的出现了fail-fast,
那么fail-fast
(快速失败)机制是什么呢?
“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。
当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。
记住是有可能,而不是一定。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。但是单线程使用不当时也可能抛出这个异常。
原文链接:https://www.jianshu.com/p/c5b52927a61a
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2017-09-08 后台常见报错处理和注意问题(一)
2017-09-08 JavaScript判断数据类型总结