天天看點

Java并發程式設計實戰系列5之基礎構模組化塊1 同步容器類2 并發容器

1 同步容器類

同步容器類包括Vector和HashTable,二者是早期JDK一部分,此外還包括在JDK 1.2中添加的一些功能相似的類,這些的同步封裝器類是由Collections.synchronizedXxx等工廠方法建立的。這些類實作線程安全的方式是:将他們的狀态封裝起來,并對每個共有方法進行同步,使得每次隻有一個線程能通路容器的狀态。

1.1 同步容器類的問題

同步容器類都是線程安全的,但在某些情況可能需額外用戶端加鎖來保護複合操作。

容器上常見的複合操作包括:

  • 疊代(反複通路元素,直到周遊完容器中所有元素)
  • 跳轉(根據指定順序找到目前元素的下一個元素)以及條件運算

在同步容器類中,這些複合操作在沒有用戶端加鎖的情況下,仍是線程安全的,

但當其他線程并發的修改容器時,他們可能會表現出意料之外的行為。

2 并發容器

Java5提供了多種并發容器來改進同步容器的性能。

同步容器将所有對容器狀态的通路都串行化,以實作他們的線程安全性。

這種方法的代價是嚴重降低并發性,當多個線程競争容器的鎖時,吞吐量将嚴重降低。

并發容器是針對多個線程并發通路設計的。在Java 5中增加了

  • ConcurrentHashMap,用來替代同步且基于散列的Map,增加了對一些常見符合操作的支援,例如“若沒有則添加”、替換以及有條件删除等。
  • CopyOnWriteArrayList,用于在周遊操作為主要操作的情況下代替同步的List。
Java并發程式設計實戰系列5之基礎構模組化塊1 同步容器類2 并發容器

copyOnWriteArrayList 和 copyOnWriteSet 一開始都共享同一個内容,當想要修改内容時,才會真正的把内容 copy 出去,形成一個新的内容後再改

比如:當我們往一個容器添加元素時,不直接往目前容器添加,而是先将容器進行 copy,複制出一個新容器,再往新容器裡加元素。添加完之後,再将原容器引用指向新容器。

好處:對 copyOnWrite 容器進行并發讀時,不需要加鎖,因為目前容器不會增加新元素,讀寫分離

copyOnWriteArrayList#add

要加鎖,否則多線程時會 copy N 個副本

copyOnWrite 适合于

讀多寫少

場景,但隻能保證資料最終一緻性,不保證明時一緻性

若你希望寫入馬上被讀到,不要用 copyOnWrite 容器

通過并發容器來代替同步容器,可以極大地提供伸縮性并降低風險。

2.1 CocurrentHashMap

同步容器在執行每個操作期間都持有一個鎖。在一些操作中,例如

HashMashMap.get

List.contains

,可能包含大量的工作:當周遊散列桶或連結清單來查找某個特定的對象時,必須在許多元素上調用equals。在基于散列的容器中,如果hashCode不能很均勻的分布散列值,那麼容器中的元素就不會均勻的分布在整個容器中。某些情況下,某個糟糕的散列函數還會把一個散清單變成線性連結清單。當周遊很長的連結清單并且在某些或者全部元素上調用equals方法時,會花費很長時間,而其他線程在這段時間内都不能通路容器。

ConcurrentHashMap使用一種粒度更細的稱為分段鎖的機制來實作更大程度的共享.

在這種機制中,任意數量的讀取線程可以并發的通路Map,執行讀操作的線程和執行寫操作的線程可以并發的通路Map,并且一定數量的寫線程可以并發的修改Map.

ConcurrentHashMap與其他并發容器一起增強了同步容器:疊代器不會抛出ConcurrentModificationException,是以疊代過程無需加鎖.

其疊代器具有"弱一緻性",而并非"及時失敗".可以容忍并發的修改,當建立疊代器時會周遊已有的元素,并可以(但不保證)在疊代器被構造後将修改操作反映給容器.

隻有當需要加鎖Map以進行獨占通路時,才應該放棄使用ConcurrentHashMap.

2.2 額外的原子Map操作

由于ConcurrentHashMap不能被加鎖來執行獨占通路,是以 無法使用用戶端加鎖來建立新的原子操作.

一些常見的複合操作,eg."若沒有則添加","若相等則移除"等,都已經實作為原子操作并且在ConcurrentMap接口中聲明,如下面代碼所示.

public interface ConcurrentMap<K, V> extends Map<K, V> {
     //僅當K沒有相應的映射值時才插入
     V putIfAbsent(K key, V value);
     
     //僅當K被映射到V時才移除
     boolean remove(Object key, Object value);
     
     //僅當K被映射到oldValue時才替換為newValue
     boolean replace(K key, V oldValue, V newValue);
     
     //僅當K被映射到某個值時才被替換為newValue
      V replace(K key, V value);
}