天天看點

并發程式設計踩坑實錄二:并發容器踩坑總結!!

并發容器

與同步容器一樣,并發容器在總體上也可以分為四大類,分别為:List、Set、Map和Queue。總體上如下圖所示。

并發程式設計踩坑實錄二:并發容器踩坑總結!!

并發容器中的List相對來說比較簡單,就一個CopyOnWriteArrayList。大家可以從字面的意思中就能夠體會到:CopyOnWrite,在寫的時候進行複制操作,也就是說在進行寫操作時,會将共享變量複制一份。那這樣做有什麼好處呢?最大的好處就是:讀操作可以做到完全無鎖化。

在CopyOnWriteArrayList内部維護了一個數組,成員變量array指向這個數組,其核心源代碼如下所示。

private transient volatile Object[] array;
final Object[] getArray() {
 return array;
}
final void setArray(Object[] a) {
 array = a;
}      

當進行操作時,都是基于array指向的這個内部數組進行的。例如,我們使用Iterator疊代器周遊這個數組時,會按照下圖所示的方式進行讀操作。

如果在周遊CopyOnWriteArrayList時發生寫操作,例如,向數組中增加一個元素時,CopyOnWriteArrayList則會将内部的數組複制一份出來,然後會在新複制出來的數組上添加新的元素,添加完再将array指向新的數組,如下圖所示。

并發程式設計踩坑實錄二:并發容器踩坑總結!!

對​于CopyOnWriteArrayList的其他寫操作和添加元素的操作原理相同,這裡就不再贅述了。

使用CopyOnWriteArrayList時需要注意的是:

  • CopyOnWriteArrayList隻适合寫操作比較少的場景,并且能夠容忍讀寫操作在短時間内的不一緻。
  • CopyOnWriteArrayList的疊代器是隻讀的,不支援寫操作。

Set

對于Set接口來說,并發容器中主要有兩個實作類,一個是CopyOnWriteArraySet,另一個是ConcurrentSkipListSet。

其中,CopyOnWriteArraySet的使用場景、原理與注意事項和CopyOnWriteArrayList一緻。

而ConcurrentSkipListSet的使用場景、原理和注意事項和下文的ConcurrentSkipListMap一緻。這裡,我就不再贅述啦。

Map

在并發容器中,Map接口的實作類主要有ConcurrentHashMap和ConcurrentSkipListMap。

而ConcurrentHashMap和ConcurrentSkipListMap最大的差別就是:ConcurrentHashMap的Key是無序的,而ConcurrentSkipListMap的Key是有序的。

在使用ConcurrentHashMap和ConcurrentSkipListMap時,需要注意的是:ConcurrentHashMap和ConcurrentSkipListMap的Key和Value都不能為空。

這裡,我們可以将Map相關的類總結成一個表格,如下所示。

Map的實作類 Key是否可為空 Value是否可為空 是否是線程安全的
HashMap
TreeMap
HashTable
ConcurrentHashMap
ConcurrentSkipListMap

這樣,大家記憶起來就友善多了。

這裡,ConcurrentSkipListMap是基于“跳表”實作的,跳表的插入、删除、查詢的平均時間複雜度為O(log n),這些時間複雜度在理論上與線程數沒有關系。如果要追求性能的話,可以嘗試使用ConcurrentSkipListMap。

Queue

在Java的并發容器中,Queue相對來說比較複雜。我們先來了解幾個概念:

  • 阻塞隊列:阻塞一般就是指當隊列已滿時,入隊操作會阻塞;當隊列為空時,出隊操作就會阻塞。
  • 非阻塞隊列:隊列的入隊和出隊操作不會阻塞。
  • 單端隊列:隊列的入隊操作隻能在隊尾進行,隊列的出隊操作隻能在隊首進行。
  • 雙端隊列:隊列的入隊操作和出隊操作都可以在隊首和隊尾進行。

我們可以将上述的隊列進行組合,将隊列分為單端阻塞隊列、雙端阻塞隊列、單端非阻塞隊列和雙端非阻塞隊列。

并發程式設計踩坑實錄二:并發容器踩坑總結!!

在Java的并發容器中,會使用明顯的辨別來區分不同類型的隊列。

  • 阻塞隊列一個明顯的辨別就是使用Blocking修飾,例如,ArrayBlockingQueue和LinkedBlockingQueue都是阻塞隊列。
  • 單端隊列會使用Queue辨別,例如ArrayBlockingQueue和LinkedBlockingQueue也是單端隊列。
  • 雙端隊列會使用Deque辨別,例如LinkedBlockingDeque和ConcurrentLinkedDeque都是雙端隊列。

接下來,我們就分别簡單聊聊這四種類型的隊列。

單端阻塞隊列

在Java的并發容器中,單端阻塞隊列的主要實作是BlockingQueue,主要包括:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue和DelayQueue。

并發程式設計踩坑實錄二:并發容器踩坑總結!!

單端阻塞隊列的内部一般會有一個隊列。

在實作上,内部的隊列可以是數組,例如ArrayBlockingQueue,也可以是連結清單,例如LinkedBlockingQueue。

也可以在内部不存在隊列,例如SynchronousQueue,SynchronousQueue實作了生産者的入隊操作必須等待消費者的出隊操作完成之後才能進行。

LinkedTransferQueue內建了LinkedBlockingQueue和SynchronousQueue的優點,并且性能比LinkedBlockingQueue好。

PriorityBlockingQueue實作了按照優先級進行出隊操作,也就是說,隊列元素在PriorityBlockingQueue内部可以按照某種規則進行排序。

DelayQueue是延時隊列,實作了在一段時間後再出隊的操作。

雙端阻塞隊列

雙端阻塞隊列的實作主要是LinkedBlockingDeque。示意圖如下所示。

并發程式設計踩坑實錄二:并發容器踩坑總結!!

單端非阻塞隊列

單端非阻塞隊列的實作主要是ConcurrentLinkedQueue,示意圖如下所示。

并發程式設計踩坑實錄二:并發容器踩坑總結!!

雙端非阻塞隊列

雙端非阻塞隊列的實作主要是ConcurrentLinkedDeque,示意圖如下所示。

并發程式設計踩坑實錄二:并發容器踩坑總結!!

有界與無界隊列

使用隊列時,還要注意隊列的有界與無界問題,也就是在使用隊列時,需要注意隊列是否有容量限制。

在實際工作中,一般推薦使用有界隊列。因為無界隊列很容易導緻記憶體溢出的問題。在Java的并發容器中,隻有ArrayBlockingQueue和LinkedBlockingQueue支援有界,其他的隊列都是無界隊列。

在使用時,一定要注意記憶體溢出問題。