1. 程式人生 > >Java併發程式設計系列之二十六 ConcurrentModificationException

Java併發程式設計系列之二十六 ConcurrentModificationException

                       

在多執行緒程式的編寫中,經常能遇到ConcurrentModificationException這個異常,這個異常的意思是指出現了併發修改的情況,為了降低對程式的效能的影響,Java開發者將捕獲的這種錯誤以“善意”的方式進行提醒。這種異常一般在對容器的元素遍歷的過程中出現了對容器的寫操作(包括增加、修改和刪除操作)時出現。仔細閱讀原始碼就知道,使用迭代器遍歷元素時由一個計數器,這個計數器就是為“快速失敗”機制設計的。

那麼,問題來了,“快速失敗”(fail-fast)又是什麼鬼?就是在迭代過程中對出現併發修改的情況時丟擲異常。這個異常就是ConcurrentModificationException異常。既然會出現這種異常自然得想辦法解決這種異常。因為是併發修改導致出現的這種異常,併發修改的一個最大的問題就是沒有使用同步,試想如果對需要遍歷的容器進行加鎖,那麼併發執行的執行緒將只有一個執行緒能獲得鎖,這樣就避免了併發修改出現的“快速失敗”。但是這麼解決仍然不夠完美,因為在容器規模比較大或者在單個元素執行的時間過長的時候,那麼這些執行緒將長時間等待。那麼,線上程競爭激烈的時候很有可能出現死鎖,即使不出現死鎖或者飢餓等情況,長時間的加鎖對系統的效能造成很大影響。在類似淘寶這樣的高併發的網站這樣設計將是一個災難。

OK,本著talk is important,show me the code as well(樓主認為溝通也是一項很重要的技能,故不是很認可之前的talk is cheap,show me the code,因為talk並不cheap)的原則,先來看看ConcurrentModificationException異常發生的一個例子:

package com.rhwayfun.patchwork.concurrency.r0408;import java.util.HashSet;import java.util.Random;import java.util.Set;import java.util.concurrent.ExecutorService;import
java.util.concurrent.Executors;/** * Created by rhwayfun on 16-4-8. */public class HiddenUnSafeIterator {    private final Set<Integer> set = new HashSet<>();    public synchronized void add(Integer i){set.add(i);}    public synchronized void remove(Integer i){set.remove(i);}    public void addTenThings(){        Random random = new Random();        for (int i = 0; i < 10; i++)            add(random.nextInt(100));        System.out.println("DEBUG: added ten elements to " + set);    }    public static void main(String[] args){        final HiddenUnSafeIterator hi = new HiddenUnSafeIterator();        ExecutorService threadPool = Executors.newFixedThreadPool(5);        for (int i = 0; i < 15; i++){            threadPool.execute(new Runnable() {                @Override                public void run() {                    hi.addTenThings();                }            });        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

執行該程式沒果然不出意料出現了ConcurrentModificationException異常。你會發現在add(Integer i)方法上添加了synchronized進行同步啊,結果應當不會出現這個異常啊,對不對?從丟擲的異常知道這樣分析是錯誤的,主要在於執行System.out.println("DEBUG: added ten elements to " + set);這個操作的時候,實際上在輸出set時,呼叫了AbstractCollection的toString方法,對容器的元素進行迭代,但是最本質的原因在於迭代的過程中沒有獲取HiddenUnSafeIterator的鎖,因為加在add方法上的synchronized獲取的鎖就是HiddenUnSafeIterator物件的物件級別鎖。這樣,雖然表面上是同步的,但是在迭代過程並沒有實際獲得鎖。這就是這個程式丟擲ConcurrentModificationException異常的本質原因。

現在,已經知道上面的程式丟擲ConcurrentModificationException異常的本質原因是沒有獲得HiddenUnSafeIterator的物件鎖。那麼只要設計程式,使得對Set容器進行迭代時能夠獲取物件鎖就可以了,在jdk中,預設為我們提供了具有同步功能的synchronizedSet方法。下面就是實現一個安全版本的迭代器,程式碼修改如下:

package com.rhwayfun.patchwork.concurrency.r0408;import java.util.Collections;import java.util.HashSet;import java.util.Random;import java.util.Set;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * Created by rhwayfun on 16-4-8. */public class SafeInterator {    //修改的地方在這裡    private final Set<Integer> set = Collections.synchronizedSet(new HashSet<Integer>());    //取消了synchronized    public void add(Integer i){set.add(i);}    public void remove(Integer i){set.remove(i);}    public void addTenThings(){        Random random = new Random();        for (int i = 0; i < 10; i++)            add(random.nextInt(100));        System.out.println("DEBUG: added ten elements to " + set);    }    public static void main(String[] args){        final SafeInterator hi = new SafeInterator();        ExecutorService threadPool = Executors.newFixedThreadPool(5);        for (int i = 0; i < 25; i++){            threadPool.execute(new Runnable() {                @Override                public void run() {                    hi.addTenThings();                }            });        }    }}
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

執行該程式沒有出現ConcurrentModificationException異常,說明我們之前對異常的分析是正確的。這段程式碼解決ConcurrentModificationException異常的主要原因就在於在迭代容器的時候獲得了SafeInterator的物件鎖。

上面的程式雖然使用了同步解決了ConcurrentModificationException異常問題,但是試想在讀遠大於寫的場景下,對每個都需要加鎖是不是很影響效能,有沒有可以不用加鎖的方案呢?答案是就是“克隆”。將容器的副本封閉線上程內部,這樣在迭代時就不用考慮併發修改的問題,因為單執行緒不存在這個問題。具體內容將在我的另一篇文章中闡述。

           

再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智慧的隊伍中來!https://blog.csdn.net/jiangjunshow