第十五章、併發容器集合
一、同步容器與併發容器
我們知道在java.util包下提供了一些容器類,而Vector和HashTable是執行緒安全的容器類,但是這些容器實現同步的方式是通過對方法加鎖(sychronized)方式實現的,這樣讀寫均需要鎖操作,導致效能低下。而即使是Vector這樣執行緒安全的類,在面對多執行緒下的複合操作的時候也是需要通過客戶端加鎖的方式保證原子性。如下面例子說明:
public class TestVector { private Vector<String> vector; //方法一 public Object getLast(Vector vector) {int lastIndex = vector.size() - 1; return vector.get(lastIndex); } //方法二 public void deleteLast(Vector vector) { int lastIndex = vector.size() - 1; vector.remove(lastIndex); } //方法三 public Object getLastSysnchronized(Vector vector) { synchronized(vector){ int lastIndex = vector.size() - 1; return vector.get(lastIndex); } } //方法四 public void deleteLastSysnchronized(Vector vector) { synchronized (vector){ int lastIndex = vector.size() - 1; vector.remove(lastIndex); } } }
如果方法一和方法二為一個組合的話。那麼當方法一獲取到了vector
的size之後,方法二已經執行完畢,這樣就導致程式的錯誤。
如果方法三與方法四組合的話。通過鎖機制保證了在vector
上的操作的原子性。
併發容器是Java 5 提供的在多執行緒程式設計下用於代替同步容器,針對不同的應用場景進行設計,提高容器的併發訪問性,同時定義了執行緒安全的複合操作。
二、併發容器類介紹
整體架構(列舉常用的容器類)
其中,阻塞佇列(BlockingQueue)在第十三章有介紹,CopyOnWrite容器(CopyOnWritexxx)在第十六章有介紹,這裡不做過多介紹。
下面分別介紹一些常用的併發容器類和介面,因篇幅原因,這裡只介紹這些類的用途和基本的原理,不做過多的原始碼解析。
(1)、ConcurrentMap介面
ConcurrentMap介面繼承了Map介面,在Map介面的基礎上又定義了四個方法:
public interface ConcurrentMap<K, V> extends Map<K, V> { //插入元素 V putIfAbsent(K key, V value); //移除元素 boolean remove(Object key, Object value); //替換元素 boolean replace(K key, V oldValue, V newValue); //替換元素 V replace(K key, V value); }
- putIfAbsent:與原有put方法不同的是,putIfAbsent方法中如果插入的key相同,則不替換原有的value值;
- remove:與原有remove方法不同的是,新remove方法中增加了對value的判斷,如果要刪除的key-value不能與Map中原有的key-value對應上,則不會刪除該元素;
- replace(K,V,V):增加了對value值的判斷,如果key-oldValue能與Map中原有的key-value對應上,才進行替換操作;
- replace(K,V):與上面的replace不同的是,此replace不會對Map中原有的key-value進行比較,如果key存在則直接替換;
(2)、ConcurrentHashMap類
ConcurrentHashMap同HashMap一樣也是基於散列表的map,但是它提供了一種與HashTable完全不同的加鎖策略提供更高效的併發性和伸縮性。
ConcurrentHashMap在JDK 1.7 和JDK 1.8中有一些區別。這裡我們分開介紹一下。
JDK 1.7
ConcurrentHashMap在JDK 1.7中,提供了一種粒度更細的加鎖機制來實現在多執行緒下更高的效能,這種機制叫分段鎖(Lock Striping)。
提供的優點是:在併發環境下將實現更高的吞吐量,而在單執行緒環境下只損失非常小的效能。
可以這樣理解分段鎖,就是將資料分段,對每一段資料分配一把鎖。當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。
有些方法需要跨段,比如size()、isEmpty()、containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。如下圖:
ConcurrentHashMap是由Segment陣列結構和HashEntry陣列結構組成。Segment是一種可重入鎖ReentrantLock,HashEntry則用於儲存鍵值對資料。
一個ConcurrentHashMap裡包含一個Segment陣列,Segment的結構和HashMap類似,是一種陣列和連結串列結構, 一個Segment裡包含一個HashEntry陣列,每個HashEntry是一個連結串列結構的元素, 每個Segment守護著一個HashEntry數組裡的元素,當對HashEntry陣列的資料進行修改時,必須首先獲得它對應的Segment鎖。
JDK 1.8
而在JDK 1.8中,ConcurrentHashMap主要做了兩個優化:
-
-
同HashMap一樣,連結串列也會在長度達到8的時候轉化為紅黑樹,這樣可以提升大量衝突時候的查詢效率;
-
以某個位置的頭結點(連結串列的頭結點或紅黑樹的root結點)為鎖,配合自旋+CAS避免不必要的鎖開銷,進一步提升併發效能。
-
(3)、ConcurrentNavigableMap介面與ConcurrentSkipListMap類
ConcurrentNavigableMap介面繼承了NavigableMap介面,這個介面提供了針對給定搜尋目標返回最接近匹配項的導航方法。
ConcurrentNavigableMap介面的主要實現類是ConcurrentSkipListMap類。從名字上來看,它的底層使用的是跳錶(SkipList)的資料結構。關於跳錶的資料結構這裡不做太多介紹,它是一種”空間換時間“的資料結構,可以使用CAS來保證併發安全性。
JDK並沒有提供執行緒安全的List類,因為對List來說,很難去開發一個通用並且沒有併發瓶頸的執行緒安全的List。因為即使簡單的讀操作,拿contains() 這樣一個操作來說,很難想到搜尋的時候如何避免鎖住整個list。
所以退一步,JDK提供了對佇列和雙端佇列的執行緒安全的類:ConcurrentLinkedQueue和ConcurrentLinkedDeque。因為佇列相對於List來說,有更多的限制。這兩個類是使用CAS來實現執行緒安全的。
2.3、併發Set
JDK提供了ConcurrentSkipListSet,是執行緒安全的有序的集合。底層是使用ConcurrentSkipListMap實現。
谷歌的guava框架實現了一個執行緒安全的ConcurrentHashSet:
Set<String> s = Sets.newConcurrentHashSet();
參考資料