《Java并發(fā)編程實(shí)戰(zhàn)》學(xué)習(xí)筆記--同步容器類

我們平時(shí)使用的一些容器,例如ArrayList其實(shí)不是線程安全的。如果我們?cè)诙嗑€程的環(huán)境之下在沒有保證線程安全的情況之下使用它們,就有可能會(huì)發(fā)生意想不到的錯(cuò)誤赎懦。那我們?cè)撊绾谓鉀Q這個(gè)問題呢?別著急幻工,Java自早期開始励两,就為我們提供了同步容器類:

  • VectorHashtable以及繼承自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í)例盲憎。

Vector類中的某幾個(gè)公有方法

既然如此,那么在使用這些同步容器類的時(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)用者的角度來看,情況就不同了:

交替調(diào)用getList和deleteList時(shí)將拋出ArrayIndexOutOfBoundsException

如果線程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夹孔。

標(biāo)準(zhǔn)容器的toString方法將迭代容器
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市析孽,隨后出現(xiàn)的幾起案子搭伤,更是在濱河造成了極大的恐慌,老刑警劉巖袜瞬,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怜俐,死亡現(xiàn)場離奇詭異,居然都是意外死亡邓尤,警方通過查閱死者的電腦和手機(jī)拍鲤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汞扎,“玉大人季稳,你說我怎么就攤上這事〕浩牵” “怎么了景鼠?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長一忱。 經(jīng)常有香客問我莲蜘,道長,這世上最難降的妖魔是什么帘营? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任票渠,我火速辦了婚禮,結(jié)果婚禮上芬迄,老公的妹妹穿的比我還像新娘问顷。我一直安慰自己,他們只是感情好禀梳,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布杜窄。 她就那樣靜靜地躺著,像睡著了一般算途。 火紅的嫁衣襯著肌膚如雪塞耕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天嘴瓤,我揣著相機(jī)與錄音扫外,去河邊找鬼。 笑死廓脆,一個(gè)胖子當(dāng)著我的面吹牛筛谚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播停忿,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼驾讲,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了席赂?” 一聲冷哼從身側(cè)響起吮铭,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氧枣,沒想到半個(gè)月后沐兵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡便监,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年扎谎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烧董。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毁靶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逊移,到底是詐尸還是另有隱情预吆,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布胳泉,位于F島的核電站拐叉,受9級(jí)特大地震影響岩遗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凤瘦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一宿礁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蔬芥,春花似錦梆靖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至乎婿,卻和暖如春测僵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谢翎。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國打工恨课, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人岳服。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓剂公,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吊宋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纲辽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

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