摘要:JDK1.5及之後的版本中,提供的線程安全的容器,一般被稱為併發容器。與同步容器一樣,併發容器在總體上也可以分為四大類,分別為:List、Set、Map和Queue。 本文分享自華為雲社區《【高併發】要想學好併發編程,這些併發容器的坑是你必須要註意的!!(建議收藏)》,作者:冰 河 。 其實, ...
摘要:JDK1.5及之後的版本中,提供的線程安全的容器,一般被稱為併發容器。與同步容器一樣,併發容器在總體上也可以分為四大類,分別為:List、Set、Map和Queue。
本文分享自華為雲社區《【高併發】要想學好併發編程,這些併發容器的坑是你必須要註意的!!(建議收藏)》,作者:冰 河 。
其實,在JDK1.5之前的線程安全的容器,大多數都是指同步容器,使用同步容器進行併發編程時,最大的問題就是性能很差。因為同步容器中的所有方法都是使用synchronized鎖進行互斥,串列度太高了,無法真正的做到並行。
所以,在JDK1.5之後,JDK中提供了併發性能更好的容器。JDK1.5及之後的版本中,提供的線程安全的容器,一般被稱為併發容器。
併發容器
與同步容器一樣,併發容器在總體上也可以分為四大類,分別為:List、Set、Map和Queue。總體上如下圖所示。
接下來,我們分別介紹下這些併發容器在使用時的註意事項和避免踩到的坑。
List
併發容器中的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相關的類總結成一個表格,如下所示。
這樣,大家記憶起來就方便多了。
這裡,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支持有界,其他的隊列都是無界隊列。
在使用時,一定要註意記憶體溢出問題。
總結
今天我們主要介紹了JDK1.5之後提供的併發容器,主要包括:List、Set、Map和Queue,而Queue又可以分為:單端阻塞隊列、雙端阻塞隊列、單端非阻塞隊列和雙端非阻塞隊列。對於每種併發容器,我們簡單介紹了其基本原理和註意事項。