關(guān)于集合中的并發(fā)修改異常及處理方式
前言
關(guān)于集合的總結(jié),可參考如下圖:

在Java中,像ArrayList這樣的集合類使用迭代器的時(shí)候,如果在遍歷過程中直接修改集合(比如remove),可能會(huì)導(dǎo)致ConcurrentModificationException。
為什么是可能會(huì)產(chǎn)生導(dǎo)致ConcurrentModificationException?
因?yàn)樵谛薷募系慕Y(jié)構(gòu)化修改modeCount的過程中,如果修改了集合的索引(索引前移),則不會(huì)發(fā)生異常。
1、并發(fā)異常介紹
ConcurrentModificationException的核心原因是:迭代器檢測到集合在遍歷過程中被修改,導(dǎo)致狀態(tài)不一致。
1.1、for-each的本質(zhì)
在 Java 中,如果你在for-each循環(huán)(即增強(qiáng)型 for 循環(huán))中直接對集合執(zhí)行remove()操作,會(huì)拋出ConcurrentModificationException。
這是因?yàn)閒or-each循環(huán)底層依賴于迭代器(Iterator)來遍歷集合,而迭代器在設(shè)計(jì)時(shí)為了保證遍歷的一致性和安全性,對集合的結(jié)構(gòu)修改有嚴(yán)格的限制。
如下所示,for-each的本質(zhì):
for (Element e : collection) {
// do something
}
等價(jià)于:
Iterator<Element> it = collection.iterator();
while (it.hasNext()) {
Element e = it.next();
// do something
}
1.2、調(diào)用list.remove()的后果
在循環(huán)中直接調(diào)用list.remove()(element)或list.remove()(index),會(huì)直接修改集合的結(jié)構(gòu),導(dǎo)致modCount自動(dòng)遞增。
此時(shí)迭代器的expectedModCount并未更新,因此在下一次調(diào)用it.next()時(shí),會(huì)檢測到不一致并拋出異常。
2、迭代器
2.1、設(shè)計(jì)原理
Java 集合框架中的迭代器(Iterator)在設(shè)計(jì)時(shí)引入了一種并發(fā)修改檢查機(jī)制,用于在遍歷過程中檢測集合的結(jié)構(gòu)是否被外部修改。
機(jī)制的核心目的保證迭代過程中集合的一致性,防止因并發(fā)修改導(dǎo)致的不可預(yù)期行為。
1、快速失?。‵ail-Fast)策略
Java 集合的迭代器采用快速失敗策略:一旦檢測到并發(fā)修改,立即拋出異常,防止后續(xù)操作產(chǎn)生不可預(yù)知的結(jié)果??焖偈〔荒鼙WC 100% 檢測到所有并發(fā)修改,但能顯著降低錯(cuò)誤概率。
2、安全性與性能的權(quán)衡
并發(fā)檢查機(jī)制增加了少量性能開銷,但保障了迭代過程的安全性。對于高并發(fā)場景,可選擇線程安全的集合類(如ConcurrentHashMap)。
2.2、并發(fā)檢查機(jī)制的屬性
1.modCount與expectedModCount
modCount:
- 是集合類(如ArrayList、HashMap)中的一個(gè)字段,表示集合的結(jié)構(gòu)性修改次數(shù)(如添加、刪除元素)。
- 每次對集合進(jìn)行結(jié)構(gòu)性修改(如add
()、remove()),modCount會(huì)自動(dòng)遞增。
expectedModCount:
是迭代器內(nèi)部保存的一個(gè)字段,表示迭代器創(chuàng)建時(shí)集合的modCount值。
- 在調(diào)用it.next()時(shí),迭代器內(nèi)部會(huì)檢查當(dāng)前集合的modCount是否與迭代器創(chuàng)建時(shí)記錄的expectedModCount一致。
- 如果不一致,就會(huì)拋出ConcurrentModificationException。
2.檢查邏輯
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
如果集合的modCount被修改(即modCount != expectedModCount),迭代器會(huì)拋出ConcurrentModificationException,表示檢測到并發(fā)修改。
2.3、機(jī)制的工作流程
1、迭代器創(chuàng)建時(shí)
迭代器在創(chuàng)建時(shí)會(huì)記錄當(dāng)前集合的modCount值,并將其賦值給expectedModCount。
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int expectedModCount = modCount; // 記錄初始狀態(tài)
...
}
2、迭代過程中調(diào)用next()
每次調(diào)用next()方法時(shí),迭代器會(huì)檢查modCount和expectedModCount是否一致。
public E next() {
checkForComodification(); // 檢查是否被修改
...
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
3、調(diào)用iterator.remove()
如果通過迭代器的remove()方法刪除集合的元素,此時(shí)迭代器iterator會(huì)同步更新modCount和expectedModCount,確保一致性。
public void remove() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 刪除元素并更新 modCount
ArrayList.this.remove(size - 1);
cursor--;
expectedModCount = modCount; // 同步更新
}
??注意:如果直接使用list.remove(),modCount會(huì)增加,下次再調(diào)用的時(shí)候,會(huì)拋異常。
2.4、適用場景
1.單線程下的結(jié)構(gòu)修改檢測
該機(jī)制不僅適用于多線程環(huán)境,也適用于單線程中在迭代過程中直接修改集合的情況。
2.多線程下的并發(fā)修改
在多線程環(huán)境下,如果多個(gè)線程同時(shí)修改集合的元素,也可能導(dǎo)致modCount與expectedModCount不一致。
此時(shí),迭代器的并發(fā)檢查機(jī)制可以檢測到這種沖突,但并不能完全解決線程安全問題。需要結(jié)合線程安全集合(如CopyOnWriteArrayList)或同步機(jī)制。
3、解決方案
3.1、迭代器的remove()
Iterator<Element> it = list.iterator();
while (it.hasNext()) {
Element e = it.next();
if (someCondition(e)) {
it.remove(); // 安全地刪除元素
}
}
it.remove()是迭代器提供的方法,它會(huì)同步更新modCount和expectedModCount,避免異常。
3.2、普通for循環(huán) + 控制索引
for (int i = 0; i < list.size(); i++) {
Element e = list.get(i);
if (someCondition(e)) {
list.remove(i);
i--; // 刪除后索引前移
}
}
- 注意:刪除元素后需要調(diào)整索引,防止跳過元素。
3.3、CopyOnWriteArrayList
如果確實(shí)需要在遍歷過程中頻繁修改集合,可以使用線程安全的CopyOnWriteArrayList:
List<Element> list = new CopyOnWriteArrayList<>();
for (Element e : list) {
if (someCondition(e)) {
list.remove(e); // 不會(huì)拋出異常
}
}
- 該集合在修改時(shí)會(huì)復(fù)制底層數(shù)組,避免直接修改共享數(shù)據(jù),但性能開銷較大。
3.4、Collections.synchronizedlist(list)
在多線程環(huán)境中使用Collections.synchronizedList時(shí),遍歷(Iteration)并修改列表的過程中,必須手動(dòng)加鎖,以確保線程安全。
這是因?yàn)樵谀J(rèn)情況下,Collections.synchronizedList的同步機(jī)制僅覆蓋其方法調(diào)用,但不包括迭代器(Iterator)的線程安全性。
1、為什么需要手動(dòng)加鎖?
- Collections.synchronizedList的同步機(jī)制
- Collections.synchronizedList返回的列表是一個(gè)線程安全的包裝類,其所有方法(如add、remove、get等)都通過synchronized關(guān)鍵字加鎖,確保單個(gè)方法調(diào)用的線程安全。
2.迭代器(Iterator)的線程安全性缺失
- 雖然列表的方法是線程安全的,但迭代器本身并不是線程安全的。
- 例如:如果線程 A 正在遍歷列表(使用iterator
()),而線程 B 同時(shí)修改了列表(如add或remove),即使這些方法是同步的,迭代器也可能拋出ConcurrentModficationException,或者看到不一致的數(shù)據(jù)狀態(tài)。
代碼示例如下:
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 添加元素
list.add("A");
list.add("B");
// 遍歷時(shí)手動(dòng)加鎖
synchronized (list) {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (item.equals("A")) {
iterator.remove(); // 安全地移除元素
}
}
}
小結(jié):

使用迭代器提供的remove()方法、避免在for-each中直接修改集合,或選擇適合的集合類型(如CopyOnWriteArrayList),可以有效避免此類異常。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot集成MinIO實(shí)現(xiàn)大文件分片上傳的示例代碼
本文主要介紹了SpringBoot集成MinIO實(shí)現(xiàn)大文件分片上傳,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-05-05
Spring Boot應(yīng)用監(jiān)控的實(shí)戰(zhàn)教程
Spring Boot 提供運(yùn)行時(shí)的應(yīng)用監(jiān)控和管理功能,下面這篇文章主要給大家介紹了關(guān)于Spring Boot應(yīng)用監(jiān)控的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-05-05
Java數(shù)據(jù)庫連接_jdbc-odbc橋連接方式(詳解)
下面小編就為大家?guī)硪黄狫ava數(shù)據(jù)庫連接_jdbc-odbc橋連接方式(詳解)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-08-08
java 同步器SynchronousQueue詳解及實(shí)例
這篇文章主要介紹了java 同步器SynchronousQueue詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-05-05
java實(shí)現(xiàn)字符串匹配求兩個(gè)字符串的最大公共子串
這篇文章主要介紹了java實(shí)現(xiàn)求兩個(gè)字符串最大公共子串的方法,詳細(xì)的描述了兩個(gè)字符串的最大公共子串算法的實(shí)現(xiàn),需要的朋友可以參考下2016-10-10

