我們平時(shí)使用的一些容器,例如ArrayList其實(shí)不是線程安全的。如果我們?cè)诙嗑€程的環(huán)境之下在沒有保證線程安全的情況之下使用它們,就有可能會(huì)發(fā)生意想不到的錯(cuò)誤赎懦。那我們?cè)撊绾谓鉀Q這個(gè)問題呢?別著急幻工,Java自早期開始励两,就為我們提供了同步容器類:
- Vector和Hashtable以及繼承自Vector的Stack。
- Collections.synchronizedXxx等工廠方法創(chuàng)建的類囊颅。
那么它們是如何實(shí)現(xiàn)線程安全的呢当悔?
很簡單,這些同步容器類將它們所有的成員變量都設(shè)為私有的(進(jìn)行狀態(tài)封裝)踢代,并且對(duì)每個(gè)公有方法都進(jìn)行同步(在方法頭部使用Synchronized進(jìn)行聲明)從而實(shí)現(xiàn)每一次只有一個(gè)線程能夠訪問該同步容器類的實(shí)例盲憎。
既然如此,那么在使用這些同步容器類的時(shí)候是不是就高枕無憂胳挎,萬事大吉了呢饼疙?
讓我們來看看下面這兩個(gè)程序:
public static Object getLast(Vector list){
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
上面兩個(gè)方法看起來沒有一點(diǎn)問題,它們都會(huì)執(zhí)行“先檢查再運(yùn)行”的操作慕爬。每個(gè)方法都是先獲得數(shù)組的大小窑眯,然后通過結(jié)果來獲取或者刪除最后一個(gè)元素。表面上看起來無論多少個(gè)線程同時(shí)調(diào)用它們医窿,也不會(huì)破壞Vector磅甩。但從調(diào)用者的角度來看,情況就不同了:
如果線程A在包含10個(gè)元素的Vector上調(diào)用getLast姥卢,同時(shí)線程B在此Vector上調(diào)用deleteLast卷要,這些操作的交替執(zhí)行如上圖所示。getLast將拋出ArrayIndexOutOffBoundsException異常隔显。在調(diào)用size與調(diào)用getLast這兩個(gè)操作之間,Vector變小了饵逐,因此在調(diào)用size時(shí)得到的索引值將不再有效括眠。
雖然這種情況很好地遵循了Vector的規(guī)范:如果請(qǐng)求一個(gè)不存在的元素,那么將拋出一個(gè)異常倍权。但這并不是Vector調(diào)用者所期望的(即使在并發(fā)修改的情況下也不希望看到)掷豺,除非Vector一開始就是空的捞烟。
我們可以使用同步策略,即使用客戶端加鎖來保證操作的原子性:
public static Object getLast(Vector list){
synchronized(this){
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list){
synchronized(this){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
類似的還有下面這個(gè)例子:
在調(diào)用size和相應(yīng)的get之間当船,Vector的長度可能發(fā)生變化题画,這種風(fēng)險(xiǎn)在對(duì)Vector中的元素進(jìn)行迭代時(shí)仍然會(huì)出現(xiàn)。
for(int i = 0 ;i < vector.size(); i++){
doSome(vector.get(i));
}
這種迭代方法的正確性完全依賴于運(yùn)氣:我們無法保證在調(diào)用size與get直接按有沒有其他線程對(duì)所操作的這個(gè)Vector進(jìn)行了修改德频。但是這并不代表Vector就不是線程安全的苍息。Vector仍然是線程安全的,而拋出的異常也與其規(guī)范保持一致壹置。然而竞思,像讀取最后一個(gè)或者迭代等這樣簡單的操作中拋出異常并不是我們所期待的。
改進(jìn)方法:
synchronized(vector){
for(int i = 0 ;i < vector.size(); i++){
doSome(vector.get(i));
}
}
迭代器與ConcurrentModificationException
Vector是一個(gè)“古老”的容器類钞护。然而盖喷,許多“現(xiàn)代”的容器類也沒有消除復(fù)合操作中的問題。無論在直接迭代還是使用for-each循環(huán)語法中难咕,對(duì)容器類進(jìn)行迭代的標(biāo)準(zhǔn)方式都是使用Iterator课梳。然而,如果有其他線程并發(fā)地修改容器余佃,那么即使是使用迭代器也無法避免在迭代期間對(duì)容器進(jìn)行加鎖暮刃。許多同步容器類在被設(shè)計(jì)的時(shí)候并沒有考慮到被并發(fā)修改的問題,它們所表現(xiàn)出的行為是****“及時(shí)失敗”(fail - fast)****的咙冗。具體的可以參考我關(guān)于ArrayList源碼的博客沾歪,里面有對(duì)用于及時(shí)失敗機(jī)制中modCount的介紹。
我們并不希望出現(xiàn)并發(fā)修改的問題雾消,同時(shí)也不希望在迭代的過程中對(duì)容器進(jìn)行加鎖 -- 因?yàn)槌钟袃蓚€(gè)鎖可能會(huì)導(dǎo)致死鎖的問題灾搏,并且持有鎖的時(shí)間過長,那么在鎖上的競爭就會(huì)非常激烈立润,從而將極大降低吞吐量以及CPU的利用率狂窑。
如果不希望在迭代的過程中加鎖,那么一種替代的方法就是對(duì)容器進(jìn)行克隆桑腮,并在副本上進(jìn)行迭代泉哈。副本將被封閉在線程內(nèi)部,因此其他線程不會(huì)在迭代期間對(duì)其進(jìn)行修改破讨。這樣就避免拋出ConcurrentModificationException(但是在克隆容器的過程中仍需要對(duì)容器進(jìn)行加鎖)丛晦。但是在克隆容器的過程中存在著顯著的性能開銷。這種方式的好壞取決于多個(gè)因素:容器的大小提陶、在每個(gè)元素上執(zhí)行的工作烫沙、迭代操作相對(duì)于容器其他操作的調(diào)用頻率以及在響應(yīng)時(shí)間和吞吐量等方面的需求。
隱藏的迭代器
我們看看下面這個(gè)程序:
public class HiddenIterator{
@GuardedBy(this)
private final Set<Integer> set = new HashSet<Integer>();
public synchronized void add(Integer i ){ set.add(i); }
public synchronized void remove(Integer i ){ set.remove(i); }
public void addTenThings(){
Random r = new Random();
for(int i = 0 ; i < 10; i++)
add(r.nextInt());
System.out.println("DEBUG : added ten elements to" + set);
}
}
表面上看起來十分的安全隙笆,add和remove兩個(gè)方法都加上了鎖锌蓄。但是其實(shí)這里面隱藏了對(duì)容器的迭代操作:編譯器將字符串的連接操作轉(zhuǎn)換為調(diào)用StringBuilder.append(Object)升筏,而這個(gè)方法又會(huì)調(diào)用容器的toString方法,標(biāo)準(zhǔn)容器的toString方法將迭代容器瘸爽,并在每個(gè)元素上調(diào)用toString來生成容器的格式化表示您访。
addTenThings可能會(huì)拋出ConcurrentModificationException,因?yàn)樵谏烧{(diào)試消息的過程中剪决,toString將對(duì)容器進(jìn)行迭代灵汪。當(dāng)然真正的問題在于HiddenIterator不是線程安全的。在使用println中的set之前必須首先獲取HiddenIterator的鎖昼捍。如果HiddenIterator用synchronizedSet來包裝HashSet识虚,并且對(duì)同步代碼進(jìn)行封裝,那么就不會(huì)發(fā)生這種錯(cuò)誤妒茬。
正如封裝對(duì)象的狀態(tài)有利于維持不變性條件一樣担锤,封裝對(duì)象的同步機(jī)制同樣有助于確保實(shí)施同步策略
其實(shí),容器的hashCode和equals等方法也會(huì)間接地執(zhí)行迭代操作乍钻,當(dāng)容器作為另一個(gè)容器的元素或者是鍵值時(shí)肛循,就會(huì)出現(xiàn)這種情況。同樣,containsAll银择、removeAll和retainAll等方法多糠,以及把容器作為參數(shù)的構(gòu)造函數(shù),都會(huì)對(duì)容器進(jìn)行迭代浩考。所有這些間接的迭代操作都會(huì)導(dǎo)致ConcurrentModificationException夹孔。