問題
- 為什麼需要同步容器類?
- 同步容器類的優點和缺點?
- 對我們設計的啟發
同步容器類
java 中的同步容器
在Java中,同步容器主要包括2類:
1)Vector、Stack、HashTable
2)Collections類中提供的靜态工廠方法建立的類
Vector實作了List接口,Vector實際上就是一個數組,和ArrayList類似,但是Vector中的方法都是synchronized方法,即進行了同步措施。
Stack也是一個同步容器,它的方法也用synchronized進行了同步,它實際上是繼承于Vector類。
HashTable實作了Map接口,它和HashMap很相似,但是HashTable進行了同步處理,而HashMap沒有。
Collections類是一個工具提供類,注意,它和Collection不同,Collection是一個頂層的接口。
在Collections類中提供了大量的方法,比如對集合或者容器進行排序、查找等操作。
最重要的是,在它裡面提供了幾個靜态工廠方法
Collections.synchronizedXXX
來建立同步容器類,
這些類實作安全的方式是,将他們的狀态封裝起來,并對每個public方法進行同步,進而使得每次隻有一個線程能通路容器的狀态。
複合操作
同步容器類都是線程安全的,但是對于某些複合操作需要額外的加鎖來保護。常見複合操作有:疊代(反複通路元素,直到周遊所有元素)、跳轉(根據指定順序找到當期元素的下一個元素)以及條件運算(如:如沒有則添加)。
Get/Delete 線程問題
存在問題的代碼
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操作之間不會有其他操作。
Vector 上複合加鎖
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将抛出異常。這是設計上的一個權衡。
如果容器規模很大, 那麼後續線程将會等待較長的時間。調用 doSomething 時持有鎖,可能會産生死鎖。長時間對容器加鎖會降低程式的可伸縮性。持有鎖的時間過長,競争就可能越激烈,如果有多個線程在等待,那麼将極大降低吞吐量和CPU的使用率。
要想避免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 并發程式設計的藝術》
http://www.cnblogs.com/lilinwei340/p/6987008.html
https://www.cnblogs.com/dolphin0520/p/3933404.html
目錄
java多線程并發之旅-01-并發概覽