非線程安全集合類(這里的集合指容器Collection翻伺,非Set)的迭代器結(jié)合了及時(shí)失敗機(jī)制,但仍然是不安全的沮焕。這種不安全表現(xiàn)在許多方面:
- 并發(fā)修改“通扯至耄”導(dǎo)致及時(shí)失敗
- 單線程修改也可能導(dǎo)致及時(shí)失敗的“誤報(bào)”
- 迭代器會(huì)“丟失”某些并發(fā)修改行為,讓及時(shí)失敗失效
如果不了解其不安全之處就隨意使用峦树,就像給程序埋下了地雷辣辫,隨時(shí)可能引爆旦事,卻不可預(yù)知。
ArrayList是一個(gè)常用的非線程安全集合急灭,下面以基于ArrayList講解幾種代表情況姐浮。
及時(shí)失敗
及時(shí)失敗也叫快速失敗,fast-fail葬馋。
“及時(shí)失敗”的迭代器并不是一種完備的處理機(jī)制卖鲤,而只是“善意地”捕獲并發(fā)錯(cuò)誤,因此只能作為并發(fā)問題的預(yù)警指示器畴嘶。它們采用的實(shí)現(xiàn)方式是蛋逾,將計(jì)數(shù)器的變化與容器關(guān)聯(lián)起來:如果在迭代期間計(jì)數(shù)器被修改,那么hasNext或next將拋出ConcurrentModificationException窗悯。然而区匣,這種檢查是在沒有同步的情況下進(jìn)行的,因此可能會(huì)看到失效的計(jì)數(shù)器蒋院,而迭代器可能并沒有意識到已經(jīng)發(fā)生了修改亏钩。這是一種設(shè)計(jì)上的權(quán)衡,從而降低并發(fā)修改操作的檢測代碼對程序性能帶來的影響欺旧。
然而姑丑,及時(shí)失敗機(jī)制十分簡潔(簡單&清晰),同時(shí)對集合的性能影響十分小切端,所以大部分非線程安全的集合類仍然使用這種機(jī)制來進(jìn)行“善意”的提醒彻坛。
幾種非線程安全的代表情況
并發(fā)修改“通城晏洌”導(dǎo)致及時(shí)失敗
“通程ぴ妫”是因?yàn)榧皶r(shí)失敗的“善意”性質(zhì),它很多時(shí)候會(huì)給我們提醒钙蒙,但有時(shí)候也不會(huì)給出提醒茵瀑,有時(shí)候甚至給出某種意義上的錯(cuò)誤提醒。這一小節(jié)針對正常的情況躬厌,這是我們考察一個(gè)機(jī)制是否值得“采納并完善”的根本屬性马昨。
構(gòu)造下列程序:
…
private Collection users = new ArrayList(); // 所以應(yīng)使用CopyOnWriteArrayList
…
users.add(new User("張三",28));
users.add(new User("李四",25));
users.add(new User("王五",31));
…
public void run() {
Iterator itrUsers = users.iterator();
while(itrUsers.hasNext()){
System.out.println("aaaa");
User user = (User)itrUsers.next();
if(“張三”.equals(user.getName())){ // 在迭代過程中修改集合
itrUsers.remove();
} else { // 正常輸出
System.out.println(user);
}
}
}
…
忽略細(xì)節(jié),假設(shè)有多個(gè)線程在同時(shí)執(zhí)行run方法扛施,操作users集合鸿捧。這時(shí),“通掣碓”會(huì)導(dǎo)致及時(shí)失敗匙奴。這里的異常可能從next或remove方法中拋出(當(dāng)然這里是從next妄荔,因?yàn)閚ext先執(zhí)行):
private class Itr implements Iterator<E> {
…
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() { // 迭代器的remove方法
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet); // 集合的remove方法
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
…
}
實(shí)際檢查并拋出異常的是checkForComodification方法:
private class Itr implements Iterator<E> {
…
int expectedModCount = modCount;
…
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
…
}
modCount是當(dāng)前集合的版本號
泼菌,每次修改(增刪改)集合都會(huì)加 1谍肤;expectedModCount是當(dāng)前迭代器的版本號
,在迭代器實(shí)例化時(shí)初始化為modCount哗伯,只有remove方法正常執(zhí)行(不拋出異常)才可以修改這個(gè)值荒揣,與modCount保持同步。
因此焊刹,如果在線程A正常迭代的過程中系任,線程B修改了users集合,modCount就會(huì)發(fā)生變化伴澄,這時(shí)赋除,線程B的expectedModCount能夠與modCount保持同步,線程A的expectedModCount卻發(fā)現(xiàn)自己與modCount不再同步非凌,從而拋出ConcurrentModificationException異常举农。
扯遠(yuǎn)些:
對于線程安全的集合類而言,我們不希望任何失敗敞嗡。但對于非線程安全的類颁糟,有人認(rèn)為“應(yīng)該在假設(shè)線程安全的情況下使用”,所以及時(shí)失敗機(jī)制完全沒有必要喉悴;有人認(rèn)為“集合類的狀態(tài)太多(所有非線程安全域的狀態(tài)數(shù)量的乘積)棱貌,并發(fā)使用時(shí)應(yīng)該給出錯(cuò)誤提醒,否則很難排查并發(fā)問題”箕肃,所以及時(shí)失敗機(jī)制很有必要婚脱。這個(gè)問題見仁見智,個(gè)人支持后者觀點(diǎn)勺像。
所以障贸,這種及時(shí)失敗的檢查是不完備的。
單線程修改也可能導(dǎo)致及時(shí)失敗的“誤報(bào)”
多線程并發(fā)修改集合時(shí)吟宦,拋出ConcurrentModificationException異常作為及時(shí)失敗的提醒篮洁,往往是我們期望的結(jié)果。然而殃姓,如果在單線程遍歷迭代器的過程中修改了集合袁波,也會(huì)拋出ConcurrentModificationException異常,看起來發(fā)生了及時(shí)失敗蜗侈。這不是我們期望的結(jié)果篷牌,是一種及時(shí)失敗的誤報(bào)。
我們改用集合的remove方法移除user“張三”:
…
public void run() {
…
if(“張三”.equals(user.getName())){ // 在迭代過程中修改集合
users.remove(user); // itrUsers.remove();
} else { // 正常輸出
System.out.println(user);
}
…
}
…
假設(shè)只有一個(gè)線程執(zhí)行run方法踏幻,在”張三”被刪除之后枷颊,下一次執(zhí)行next方法時(shí),仍舊會(huì)拋出ConcurrentModificationException異常,也就是導(dǎo)致了及時(shí)失敗偷卧。
這時(shí)因?yàn)榧系膔emove方法并沒有維護(hù)集合修改的狀態(tài)(如對modCount&expectedModCount組合
的修改和檢查):
public class ArrayList<E> extends AbstractList<E>
…
public boolean remove(Object o) { // 集合的remove方法
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
…
}
這也讓我們更容易理解及時(shí)失敗的本質(zhì)——依托于對集合修改狀態(tài)的維護(hù)豺瘤。這里的主要原因看起來是“集合的remove方法破壞了正常維護(hù)的集合修改狀態(tài)”,但對于使用者而言听诸,集合在單線程環(huán)境下卻拋出了ConcurrentModificationException異常坐求,這是由于及時(shí)失敗機(jī)制沒有區(qū)分單線程與多線程的情況,統(tǒng)一給出同樣的提醒(拋出ConcurrentModificationException異常)晌梨,因而是及時(shí)失敗的誤報(bào)桥嗤。
迭代器會(huì)“丟失”某些并發(fā)修改行為,讓及時(shí)失敗失效
除了誤報(bào)仔蝌,及時(shí)失敗之僅限于“善意”(有提醒就是“善意”的泛领,沒有也不是“惡意”的)還體現(xiàn)在其可能“丟失”某些并發(fā)修改行為。在這里敛惊,“丟失”意味著不提醒——某些線程并發(fā)修改了當(dāng)前集合渊鞋,但沒有拋出ConcurrentModificationException異常,及時(shí)失敗機(jī)制失效了瞧挤。
主動(dòng)避過及時(shí)失敗的檢查
利用hasNext方法提前結(jié)束線程锡宋,可以主動(dòng)避過及時(shí)失敗的檢查,從而導(dǎo)致修改行為的丟失:
private class Itr implements Iterator<E> {
…
public boolean hasNext() {
return cursor != size; // 思考:如果刪除了集合的倒數(shù)第二個(gè)元素特恬,會(huì)發(fā)生什么执俩?
}
…
}
還是單線程的場景下,假設(shè)我們刪除了集合的倒數(shù)第二個(gè)元素癌刽。這時(shí)next方法導(dǎo)致cursor=oldSize-1
役首,同時(shí)remove方法導(dǎo)致newSize=oldSize-1
(oldSize是集合修改之前的size值,newSize集合修改之后的)显拜;所以hasNext方法會(huì)返回false衡奥,讓用戶誤以為集合迭代已經(jīng)結(jié)束(實(shí)際上還有最后一個(gè)元素),從而循環(huán)終止(在我們的程序里用hasNext判斷是否結(jié)束)讼油,無法拋出ConcurrentModificationException異常杰赛,及時(shí)失敗失效了呢簸。
推廣到多線程的情景是一樣的矮台,因?yàn)閟ize是共享的。
及時(shí)失敗的實(shí)現(xiàn)是非線程安全的
很容易忽略的一點(diǎn)是根时,上述集合修改狀態(tài)的維護(hù)本身就是在沒有同步的情況下進(jìn)行的瘦赫,因此可能看到更多(遠(yuǎn)比上述要多)失效的集合修改狀態(tài),使迭代器意識不到集合發(fā)生了修改蛤迎,這是一種競態(tài)條件(Race Condition)确虱。
假設(shè)線程A進(jìn)入迭代器的remove方法,線程B進(jìn)入迭代器的next方法替裆,現(xiàn)在線程A執(zhí)行集合的remove方法:
private class Itr implements Iterator<E> {
…
public void remove() {
…
ArrayList.this.remove(lastRet);
…
}
…
}
首先校辩,假設(shè)沒有其他線程并發(fā)修改窘问,則兩個(gè)線程都可以通過checkForComodification()的檢查;然后線程A快速的執(zhí)行集合的remove方法宜咒;待線程A執(zhí)行完集合的remove方法惠赫,由于線程B之前已經(jīng)通過了檢查,現(xiàn)在就無法意識到“users集合在線程A中已經(jīng)發(fā)生了變化”故黑。另外儿咱,因?yàn)閹缀跬耆淮嬖谕酱胧琺odCount的修改也存在競態(tài)條件场晶,其他狀態(tài)也無法保證是否有效混埠。
總結(jié)
上面看到了非線程安全集合類的迭代器是不安全的,但在單線程的環(huán)境下诗轻,這些集合類在性能钳宪、維護(hù)難度等方面仍然具有不可替代的優(yōu)勢。那么該如何在兼具一定程度線程安全的前提下扳炬,更好的發(fā)揮內(nèi)建集合類的優(yōu)勢呢使套?總結(jié)起來無非兩點(diǎn):
- 使用非線程安全的集合時(shí)(實(shí)際上對于某些“線程安全”的集合類,其迭代器也是線程不安全的)鞠柄,迭代過程中需要用戶自覺維護(hù)侦高,不修改該集合。
- 應(yīng)盡可能明確線程安全的需求等級厌杜,做好一致性奉呛、活躍性、性能等方面的平衡夯尽,再針對性的使用相應(yīng)的集合類瞧壮。
參考:
- 傳智播客_張孝祥_Java多線程與并發(fā)庫高級應(yīng)用視頻教程/19_傳智播客_張孝祥_java5同步集合類的應(yīng)用.avi
本文鏈接:源碼|從源碼分析非線程安全集合類的不安全迭代器
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載匙握,演繹或用于商業(yè)目的咆槽,但是必須保留本文的署名及鏈接。