ConcurentmodifycationException

在 Java 開發(fā)手冊中穷绵,有這樣一條規(guī)定:


image.png

但是手冊中并沒有給出具體原因淮摔,本文就來深入分析一下該規(guī)定背后的思考。

foreach 循環(huán)

Foreach 循環(huán)(Foreach loop)是計算機編程語言中的一種控制流程語句,通常
用來循環(huán)遍歷數(shù)組或集合中的元素。
Java 語 言 從 JDK 1.5.0 開 始 引 入 foreach 循 環(huán)于购。 在 遍 歷 數(shù) 組、 集 合 方 面知染,
為什么禁止在 foreach 循環(huán)里進行元素的 remove/add 操作肋僧? <  61
foreach 為開發(fā)人員提供了極大的方便。
foreach 語法格式如下:

for( 元素類型 t 元素變量 x : 遍歷對象 obj){
 引用了 x 的 java 語句 ;
} 

以下實例演示了 普通 for 循環(huán) 和 foreach 循環(huán)使用:

public static void main(String[] args) {
 // 使用 ImmutableList 初始化一個 List
 List<String> userNames = ImmutableList.of("Hollis", "hollis",
"HollisChuang", "H");
 System.out.println(" 使用 for 循環(huán)遍歷 List");
 for (int i = 0; i < userNames.size(); i++) {
 System.out.println(userNames.get(i));
 }
 System.out.println(" 使用 foreach 遍歷 List");
 for (String userName : userNames) {
 System.out.println(userName);
 }
}

以上代碼運行輸出結(jié)果為:

使用 for 循環(huán)遍歷 List
Hollis
hollis
HollisChuang
H
使用 foreach 遍歷 List
Hollis
hollis
HollisChuang
H

可以看到,使用 foreach 語法遍歷集合或者數(shù)組的時候嫌吠,可以起到和普通 for
循環(huán)同樣的效果止潘,并且代碼更加簡潔。所以辫诅,foreach 循環(huán)也通常也被稱為增強 for
循環(huán)凭戴。
但是,作為一個合格的程序員泥栖,我們不僅要知道什么是增強 for 循環(huán)簇宽,還需要知
道增強 for 循環(huán)的原理是什么?
其實吧享,增強 for 循環(huán)也是 Java 給我們提供的一個語法糖,如果將以上代碼編譯
后的 class 文件進行反編譯(使用 jad 工具)的話譬嚣,可以得到以下代碼

Iterator iterator = userNames.iterator();
do
{
 if(!iterator.hasNext())
 break;
 String userName = (String)iterator.next();
 if(userName.equals("Hollis"))
 userNames.remove(userName);
} while(true);
System.out.println(userNames);

可以發(fā)現(xiàn)钢颂,原本的增強 for 循環(huán),其實是依賴了 while 循環(huán)和 Iterator 實現(xiàn)的拜银。
(請記住這種實現(xiàn)方式殊鞭,后面會用到!)

問題重現(xiàn)

規(guī)范中指出不讓我們在 foreach 循環(huán)中對集合元素做 add/remove 操作尼桶,那么操灿,
我們嘗試著做一下看看會發(fā)生什么問題。

// 使用雙括弧語法(double-brace syntax)建立并初始化一個 List
List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
}};
for (int i = 0; i < userNames.size(); i++) {
 if (userNames.get(i).equals("Hollis")) {
 userNames.remove(i);
 }
}
System.out.println(userNames);

以上代碼泵督,首先使用雙括弧語法(double-brace syntax)建立并初始化一個
List趾盐,其中包含四個字符串,分別是 Hollis小腊、hollis救鲤、HollisChuang 和 H。
然后使用普通 for 循環(huán)對 List 進行遍歷秩冈,刪除 List 中元素內(nèi)容等于 Hollis 的元
素本缠。然后輸出 List,輸出結(jié)果如下:

[hollis, HollisChuang, H]

以上是哪使用普通的 for 循環(huán)在遍歷的同時進行刪除入问,那么丹锹,我們再看下,如果
使用增強 for 循環(huán)的話會發(fā)生什么:

List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
}};
for (String userName : userNames) {
 if (userName.equals("Hollis")) {
 userNames.remove(userName);
 }
}
System.out.println(userNames);

以上代碼芬失,使用增強 for 循環(huán)遍歷元素楣黍,并嘗試刪除其中的 Hollis 字符串元素。
運行以上代碼麸折,會拋出以下異常:

java.util.ConcurrentModificationException

同樣的锡凝,讀者可以嘗試下在增強 for 循環(huán)中使用 add 方法添加元素,結(jié)果也會同
樣拋出該異常垢啼。
之所以會出現(xiàn)這個異常窜锯,是因為觸發(fā)了一個 Java 集合的錯誤檢測機制——failfast 张肾。

fail-fast

接下來,我們就來分析下在增強 for 循環(huán)中 add/remove 元素的時候會拋出
java.util.ConcurrentModificationException 的原因锚扎,即解釋下到底什么是 fail-fast
進制吞瞪,fail-fast 的原理等。
fail-fast驾孔,即快速失敗芍秆,它是 Java 集合的一種錯誤檢測機制。當多個線程對集
合(非 fail-safe 的集合類)進行結(jié)構(gòu)上的改變的操作時翠勉,有可能會產(chǎn)生 fail-fast 機
制妖啥,這個時候就會拋出 ConcurrentModificationException(當方法檢測到對象的并
發(fā)修改,但不允許這種修改時就拋出該異常)对碌。
同時需要注意的是荆虱,即使不是多線程環(huán)境,如果單線程違反了規(guī)則朽们,同樣也有可
能會拋出改異常怀读。

那么,在增強 for 循環(huán)進行元素刪除骑脱,是如何違反了規(guī)則的呢菜枷?
要分析這個問題,我們先將增強 for 循環(huán)這個語法糖進行解糖叁丧,得到以下代碼:

public static void main(String[] args) {
 // 使用 ImmutableList 初始化一個 List
 List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
 }};
 Iterator iterator = userNames.iterator();
 do
 {
 if(!iterator.hasNext())
 break;
 String userName = (String)iterator.next();
 if(userName.equals("Hollis"))
 userNames.remove(userName);
 } while(true);
 System.out.println(userNames);
}

然后運行以上代碼啤誊,同樣會拋出異常。我們來看一下 ConcurrentModificationException 的完整堆棧:


image.png

通過異常堆棧我們可以到歹袁,異常發(fā)生的調(diào)用鏈 ForEachDemo 的第 23 行坷衍,
Iterator.next 調(diào)用了 Iterator.checkForComodification 方法 ,而異常就
是 checkForComodification 方法中拋出的条舔。
其 實枫耳, 經(jīng) 過 debug 后, 我 們 可 以 發(fā) 現(xiàn)孟抗, 如 果 remove 代 碼 沒 有 被 執(zhí) 行 過迁杨,
iterator.next 這一行是一直沒報錯的。拋異常的時機也正是 remove 執(zhí)行之后的的那
一次 next 方法的調(diào)用凄硼。
我們直接看下 checkForComodification 方法的代碼铅协,看下拋出異常的原因:

final void checkForComodification() {
 if (modCount != expectedModCount)
 throw new ConcurrentModificationException();
}

代 碼 比 較 簡 單,modCount != expectedModCount 的 時 候摊沉, 就 會 拋 出
ConcurrentModificationException狐史。
那么,就來看一下,remove/add 操作室如何導致 modCount 和 expectedModCount 不相等的吧骏全。

remove/add 做了什么

首先苍柏,我們要搞清楚的是,到底 modCount 和 expectedModCount 這兩個變
量都是個什么東西姜贡。
通過翻源碼试吁,我們可以發(fā)現(xiàn):
● modCount 是 ArrayList 中的一個成員變量。它表示該集合實際被修改的次
數(shù)楼咳。
● expectedModCount 是 ArrayList 中的一個內(nèi)部類——Itr 中的成員變量熄捍。
expectedModCount 表示這個迭代器期望該集合被修改的次數(shù)。其值是在
ArrayList.iterator 方法被調(diào)用的時候初始化的母怜。只有通過迭代器對集合進行操
作余耽,該值才會改變。
● Itr 是一個 Iterator 的實現(xiàn)糙申,使用 ArrayList.iterator 方法可以獲取到的迭代器
就是 Itr 類的實例宾添。
他們之間的關(guān)系如下:
class ArrayList{
private int modCount;
public void add();
public void remove();
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
}
public Iterator<E> iterator() {
return new Itr();
}
}
其實,看到這里柜裸,大概很多人都能猜到為什么 remove/add 操作之后,會導致
expectedModCount 和 modCount 不想等了粱锐。
通過翻閱代碼疙挺,我們也可以發(fā)現(xiàn),remove 方法核心邏輯如下:


image.png

可以看到怜浅,它只修改了 modCount铐然,并沒有對 expectedModCount 做任何
操作。
簡單總結(jié)一下恶座,之所以會拋出 ConcurrentModificationException 異常搀暑,是因
為我們的代碼中使用了增強 for 循環(huán),而在增強 for 循環(huán)中跨琳,集合遍歷是通過 iterator
進行的自点,但是元素的 add/remove 卻是直接使用的集合類自己的方法。這就導致
iterator 在遍歷的時候脉让,會發(fā)現(xiàn)有一個元素在自己不知不覺的情況下就被刪除 / 添加
了桂敛,就會拋出一個異常,用來提示用戶溅潜,可能發(fā)生了并發(fā)修改术唬!

正確姿勢

至此,我們介紹清楚了不能在 foreach 循環(huán)體中直接對集合進行 add/remove
操作的原因滚澜。
但是粗仓,很多時候,我們是有需求需要過濾集合的,比如刪除其中一部分元素借浊,那
么應該如何做呢塘淑?有幾種方法可供參考:

1. 直接使用普通 for 循環(huán)進行操作

我們說不能在 foreach 中進行,但是使用普通的 for 循環(huán)還是可以的巴碗,因為普通
for 循環(huán)并沒有用到 Iterator 的遍歷朴爬,所以壓根就沒有進行 fail-fast 的檢驗。

 List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
 }};
 for (int i = 0; i < 1; i++) {
 if (userNames.get(i).equals("Hollis")) {
 userNames.remove(i);
 }
 }
 System.out.println(userNames);

這種方案其實存在一個問題橡淆,那就是 remove 操作會改變 List 中元素的下標召噩,
可能存在漏刪的情況。

2. 直接使用 Iterator 進行操作

除了直接使用普通 for 循環(huán)以外逸爵,我們還可以直接使用 Iterator 提供的 remove
方法具滴。

List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
 }};
 Iterator iterator = userNames.iterator();
 while (iterator.hasNext()) {
 if (iterator.next().equals("Hollis")) {
 iterator.remove();
 }
 }
 System.out.println(userNames);

如果直接使用 Iterator 提供的 remove 方法,那么就可以修改到 expectedModCount 的值师倔。那么就不會再拋出異常了构韵。其實現(xiàn)代碼如下:


image.png

3. 使用 Java 8 中提供的 filter 過濾

Java 8 中可以把集合轉(zhuǎn)換成流,對于流有一種 filter 操作趋艘, 可以對原始 Stream
進行某項測試疲恢,通過測試的元素被留下來生成一個新 Stream。

 List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
 }};
 userNames = userNames.stream().filter(userName -> !userName.
equals("Hollis")).collect(Collectors.toList());
 System.out.println(userNames);

4. 使用增強 for 循環(huán)其實也可以

如果瓷胧,我們非常確定在一個集合中显拳,某個即將刪除的元素只包含一個的話, 比如
對 Set 進行操作搓萧,那么其實也是可以使用增強 for 循環(huán)的杂数,只要在刪除之后,立刻結(jié)
束循環(huán)體瘸洛,不要再繼續(xù)進行遍歷就可以了揍移,也就是說不讓代碼執(zhí)行到下一次的 next
方法。

List<String> userNames = new ArrayList<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
 }};
 for (String userName : userNames) {
 if (userName.equals("Hollis")) {
 userNames.remove(userName);
 break;
 }
 }
 System.out.println(userNames);

5. 直接使用 fail-safe 的集合類

在 Java 中反肋,除了一些普通的集合類以外那伐,還有一些采用了 fail-safe 機制的集
合類。這樣的集合容器在遍歷時不是直接在集合內(nèi)容上訪問的囚玫,而是先復制原有集合
內(nèi)容喧锦,在拷貝的集合上進行遍歷。
由于迭代時是對原集合的拷貝進行遍歷抓督,所以在遍歷過程中對原集合所作的修改
并不能被迭代器檢測到燃少,所以不會觸發(fā) ConcurrentModificationException。

ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
 add("Hollis");
 add("hollis");
 add("HollisChuang");
 add("H");
}};
for (String userName : userNames) {
 if (userName.equals("Hollis")) {
 userNames.remove();
 }
}

基于拷貝內(nèi)容的優(yōu)點是避免了 ConcurrentModificationException铃在,但同樣地阵具,
迭代器并不能訪問到修改后的內(nèi)容碍遍,即:迭代器遍歷的是開始遍歷那一刻拿到的集合
拷貝,在遍歷期間原集合發(fā)生的修改迭代器是不知道的阳液。
為什么禁止在 foreach 循環(huán)里進行元素的 remove/add 操作怕敬? <  71
java.util.concurrent 包下的容器都是安全失敗,可以在多線程下并發(fā)使用帘皿,并
發(fā)修改东跪。

總結(jié)

我們使用的增強 for 循環(huán),其實是 Java 提供的語法糖鹰溜,其實現(xiàn)原理是借助
Iterator 進行元素的遍歷虽填。
但是如果在遍歷過程中,不通過 Iterator曹动,而是通過集合類自身的方法對集合進
行添加 / 刪除操作斋日。那么在 Iterator 進行下一次的遍歷時,經(jīng)檢測發(fā)現(xiàn)有一次集合的
修改操作并未通過自身進行墓陈,那么可能是發(fā)生了并發(fā)被其他線程執(zhí)行的恶守,這時候就會
拋出異常,來提示用戶可能發(fā)生了并發(fā)修改贡必,這就是所謂的 fail-fast 機制兔港。
當然還是有很多種方法可以解決這類問題的。比如使用普通 for 循環(huán)仔拟、使用
Iterator 進行元素刪除押框、使用 Stream 的 filter、使用 fail-safe 的類等理逊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市盒揉,隨后出現(xiàn)的幾起案子晋被,更是在濱河造成了極大的恐慌,老刑警劉巖刚盈,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件羡洛,死亡現(xiàn)場離奇詭異,居然都是意外死亡藕漱,警方通過查閱死者的電腦和手機欲侮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肋联,“玉大人威蕉,你說我怎么就攤上這事¢先裕” “怎么了韧涨?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵牍戚,是天一觀的道長。 經(jīng)常有香客問我虑粥,道長如孝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任娩贷,我火速辦了婚禮第晰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘彬祖。我一直安慰自己茁瘦,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布涧至。 她就那樣靜靜地躺著腹躁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪南蓬。 梳的紋絲不亂的頭發(fā)上纺非,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音赘方,去河邊找鬼烧颖。 笑死,一個胖子當著我的面吹牛窄陡,可吹牛的內(nèi)容都是我干的炕淮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼跳夭,長吁一口氣:“原來是場噩夢啊……” “哼涂圆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起币叹,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤润歉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后颈抚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踩衩,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年贩汉,在試婚紗的時候發(fā)現(xiàn)自己被綠了驱富。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡匹舞,死狀恐怖褐鸥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情策菜,我是刑警寧澤晶疼,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布酒贬,位于F島的核電站,受9級特大地震影響翠霍,放射性物質(zhì)發(fā)生泄漏锭吨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一寒匙、第九天 我趴在偏房一處隱蔽的房頂上張望零如。 院中可真熱鬧,春花似錦锄弱、人聲如沸考蕾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肖卧。三九已至,卻和暖如春掸鹅,著一層夾襖步出監(jiān)牢的瞬間塞帐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工巍沙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留葵姥,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓句携,卻偏偏與公主長得像榔幸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子矮嫉,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容