1. 程式人生 > >java並發-同步容器類

java並發-同步容器類

隱藏 color 方式 ngs 順序 訪問 ++ ica 平臺

  java平臺類庫包含了豐富的並發基礎構建模塊,如線程安全的容器類以及各種用於協調多個相互協作的線程控制流的同步工具類。

同步容器類

  同步容器類包括Vector和Hashtable,是早期JDK的一部分,此外還有Collections.synchronizedXXX等工廠方法創建的。這些類實現安全的方式是,將他們的狀態封裝起來,並對每個public方法進行同步,從而 使得每次只有一個線程能訪問容器的狀態。

  同步容器類都是線程安全的,但是對於某些復合操作需要額外的加鎖來保護。常見復合操作有:叠代(反復訪問元素,直到遍歷所有元素)、跳轉(根據指定順序找到當期元素的下一個元素)以及條件運算(如:如沒有則添加)。

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);
}

  上面例子中,Vector中定義了兩個方法,它們都執行先檢查再運行操作。先獲取數組大小,再獲取或刪除最後一個元素。這些方法看似沒問題,並且都是線程安全的,也不破壞Vector。但是從調用者角度來看,就有問題了。可能A線程調用getLast的過程中,B線程調用了deleteLast,Vector元素減少,導致A線程調用失敗。

  同步容器類遵守同步策略,即支持客戶端加鎖,因此只要我們知道應該使用那個鎖,就能創建一些新的操作。這些新操作與容器與其他操作都是原子操作。同步容器通過自身的鎖來保護它的每個方法。通過獲取容器的鎖,就能使上面的方法稱為原子操作。size和get操作之間不會有其他操作。

public static Object getLast(Vector list){
    synchronized(list){
        int lastIndex = list.size() - 1;
        return list.get(lastIndex)
    }
    
}

public
static void deleteLast(Vector list){ synchronized(list){ int lastIndex = list.size() - 1; list.remove(lastIndex); } }

  同樣的問題也會出現在遍歷上,如下面的例子:

for(int i=0; i< vector.size(); i++)
    doSomgthing(vector.get(i));

  如果另外一個線程刪除一個元素,會導致ArrayIndexOutBoundsException異常。我們可以通過加鎖來解決叠代不可靠問題,避免其他線程在遍歷過程修改Vector。但也帶來性能問題,叠代期間其他線程無法訪問它。

synchronized(vector){
    for(int i=0; i< vector.size(); i++)
        doSomgthing(vector.get(i));
}

  上面的例子在未加鎖的情況下都可能拋出異常,這並不意味著Vector不是線程安全的。Vector仍然是線程安全的,拋出的異常也與其規範保持一致。

叠代器與ConcurrentModificationException

  對容器進行叠代的標準方式是使用Iterator,使用for-each語法,也是調用Iterator。在設計同步容器類的時候並沒有考慮並發修改問題,它們表現出的行為是“及時失敗”的,意味著在叠代過程中,如果有其他線程修改容器,會拋出ConcurrentModificationException異常。它們實現的方式是,將計數器變化與容器關聯起來,放叠代器件計數器被修改,那麽hasNext或next將拋出異常。這是設計上的一個權衡。

  要想避免ConcurrentModificationException,就必須在叠代過程持有容器的鎖。但是如果容器規模很大,叠代過程持有鎖,將導致嚴重的性能問題。一種替代方式就是“克隆容器”,並在副本上叠代。克隆過程仍然需要加鎖,同時存在顯著的性能開銷。克隆容器的好壞取決於過個元素,如容器大小,叠代時,每個元素執行的操作等。

隱藏叠代器

  加鎖可以防止叠代拋出ConcurrentModificationException異常,但是需要在所有叠代的地方進行加鎖。實際情況通常更加復雜,有些情況下可能會忽略隱藏的叠代器。

  

public class HiddenIterator{
    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 r = new Random();
        for(int i=0;i<10;i++){
            add(r.nextInt());
        }
        System.out.println("debug" + set)
    }
}

  addTenThings方法可能拋出ConcurrentModificationException異常,因為在打印輸出的時候進行字符串連接,會調用set的toString方法,toString方法會對容器進行叠代。在使用println前必須獲取HiddenIterator的鎖,但是實際應用中可能忽略。

  封裝對象的狀態有助於維持不變性,封裝對象的同步機制有助有確保實施同步策略。

  如果使用synchronizedSet來包裝HashSet,並且對同步代碼進行封裝,就不會發生這種錯誤。

  除了toString,hashCode和equals等方法也會間接執行叠代操作。當容器作為另一個容器的元素和鍵值時,就會出現這種情況。同樣,containsAll,removeAll等方法,以及把容器作為參數的構造函數都會對容器進行叠代。這些間接操作都有可能拋出ConcurrentModificationException異常。

java並發-同步容器類