1. 程式人生 > 其它 >Java併發容器簡介

Java併發容器簡介

一、容器介紹

Java中的容器主要可以分為四大類,分別是ListMapSetQueue,不考慮多執行緒併發的情況下,容器類一般使用ArrayListHashMap等執行緒不安全的類,效率更高。在併發場景下,常會用到ConcurrentHashMapArrayBlockingQueue等執行緒安全的容器類,雖然犧牲了一些效率,但卻得到了安全。

List list = Collections.
  synchronizedList(new ArrayList());
Set set = Collections.
  synchronizedSet(new HashSet());
Map map = Collections.
  synchronizedMap(new HashMap());

上面提到的這些經過包裝後執行緒安全容器,都是基於synchronized這個同步關鍵字實現,所以也被稱為同步容器,Java提供的同步容器還有VectorStackHashtable,這三個容器並不是基於包裝類實現,但同樣是基於synchronized實現的,對這三個容器的遍歷,同樣要加鎖保證互斥。

1.1 併發容器介紹

Java在1.5版本之前所謂的執行緒安全的容器,主要指的就是同步容器。不過同步容器有個最大的問題,那就是效能差,所有方法都用synchronized來保證互斥,序列度太高了。因此Java在1.5及之後版本提供了效能更高的容器,我們一般稱為併發容器。

ConcurrentHashMap:併發版HashMap
CopyOnWriteArrayList:併發版ArrayList
CopyOnWriteArraySet:併發Set
ConcurrentLinkedQueue:併發佇列(基於連結串列)
ConcurrentLinkedDeque:併發佇列(基於雙向連結串列)
ConcurrentSkipListMap:基於跳錶的併發Map
ConcurrentSkipListSet:基於跳錶的併發Set
ArrayBlockingQueue:阻塞佇列(基於陣列)
LinkedBlockingQueue:阻塞佇列(基於連結串列)
LinkedBlockingDeque:阻塞佇列(基於雙向連結串列)
PriorityBlockingQueue:執行緒安全的優先佇列
SynchronousQueue:讀寫成對的佇列
LinkedTransferQueue:基於連結串列的資料交換佇列
DelayQueue:延時佇列

二、List

2.1 CopyOnWriteArrayList

併發版ArrayList,底層結構也是陣列,和ArrayList不同之處在於:當新增和刪除元素時會建立一個新的陣列,在新的陣列中增加或者排除指定物件,最後用新增陣列替換原來的陣列。

適用場景:由於讀操作不加鎖,寫(增、刪、改)操作加鎖,因此適用於讀多寫少的場景。

侷限:由於讀的時候不會加鎖(讀的效率高,就和普通ArrayList一樣),讀取的當前副本,因此可能讀取到髒資料。如果介意,建議不用。

三、Map

3.1 ConcurrentHashMap

最常見的併發容器之一,可以用作併發場景下的快取。底層依然是雜湊表,但在JAVA 8

中有了不小的改變,而JAVA 7JAVA 8都是用的比較多的版本,因此經常會將這兩個版本的實現方式做一些比較(比如面試中)。

一個比較大的差異就是,JAVA 7中採用分段鎖來減少鎖的競爭,JAVA 8中放棄了分段鎖,採用CAS(一種樂觀鎖),同時為了防止雜湊衝突嚴重時退化成連結串列(衝突時會在該位置生成一個連結串列,雜湊值相同的物件就鏈在一起),會在連結串列長度達到閾值8後轉換成紅黑樹(比起連結串列,樹的查詢效率更穩定)。

3.2 ConcurrentSkipListMap

SkipList即跳錶,跳錶是一種空間換時間的資料結構,通過冗餘資料,將連結串列一層一層索引,達到類似二分查詢的效果

四、Set

4.1 CopyOnWriteArraySet

基於CopyOnWriteArrayList實現(內含一個CopyOnWriteArrayList成員變數),也就是說底層是一個數組,意味著每次add都要遍歷整個集合才能知道是否存在,不存在時需要插入(加鎖)。

適用場景:在CopyOnWriteArrayList適用場景下加一個,集合別太大(全部遍歷傷不起)。

4.2 ConcurrentSkipListSet

類似HashSetHashMap的關係,ConcurrentSkipListSet裡面就是一個ConcurrentSkipListMap,就不細說了。

五、Queue

5.1 BlockingQueue

5.1.1 ArrayBlockingQueue

基於陣列實現的可阻塞佇列,構造時必須制定陣列大小,往裡面放東西時如果陣列滿了便會阻塞直到有位置(也支援直接返回和超時等待),通過一個鎖ReentrantLock保證執行緒安全。

offer操作舉個例子:

乍一看會有點疑惑,讀和寫都是同一個鎖,那要是空的時候正好一個讀執行緒來了不會一直阻塞嗎?

答案就在notEmptynotFull裡,這兩個出自lock的小東西讓鎖有了類似synchronized + wait + notify的功能。

5.1.2 LinkedBlockingQueue

基於連結串列實現的阻塞佇列,想比與不阻塞的ConcurrentLinkedQueue,它多了一個容量限制,如果不設定預設為int最大值。

5.1.3 SynchronousQueue

一個虛假的佇列,因為它實際上沒有真正用於儲存元素的空間,每個插入操作都必須有對應的取出操作,沒取出時無法繼續放入。

一個簡單的例子感受一下:

可以看到,寫入的執行緒沒有任何sleep,可以說是全力往佇列放東西,而讀取的執行緒又很不積極,讀一個又sleep一會。輸出的結果卻是讀寫操作成對出現。

JAVA中一個使用場景就是

Executors.newCachedThreadPool(),建立一個快取執行緒池。

5.1.4 LinkedTransferQueue

實現了介面TransferQueue,通過transfer方法放入元素時,如果發現有執行緒在阻塞在取元素,會直接把這個元素給等待執行緒。如果沒有人等著消費,那麼會把這個元素放到佇列尾部,並且此方法阻塞直到有人讀取這個元素。和SynchronousQueue有點像,但比它更強大。

5.1.5 PriorityBlockingQueue

構造時可以傳入一個比較器,可以看做放進去的元素會被排序,然後讀取的時候按順序消費。某些低優先順序的元素可能長期無法被消費,因為不斷有更高優先順序的元素進來。

5.1.6 DelayQueue

可以使放入佇列的元素在指定的延時後才被消費者取出,元素需要實現Delayed介面。

5.2 BlockingDeque

5.2.1 LinkedBlockingDeque

類似LinkedBlockingQueue,但提供了雙向連結串列特有的操作。

5.3 ConcurrentLinkedQueue

基於連結串列實現的併發佇列,使用樂觀鎖(CAS)保證執行緒安全。因為資料結構是連結串列,所以理論上是沒有佇列大小限制的,也就是說新增資料一定能成功。

5.4 ConcurrentLinkedDeque

基於雙向連結串列實現的併發佇列,可以分別對頭尾進行操作,因此除了先進先出(FIFO),也可以先進後出(FILO),當然先進後出的話應該叫它棧了。

六、總結

上面簡單介紹了JAVA併發包下的一些容器類,知道有這些東西,遇到合適的場景時就能想起有個現成的東西可以用了。想要知其所以然,後續還得再深入探索一番。