Java集合之并发容器
一:java中的并發容器總結
JDK提供的這些容器大部分在 java.util.concurrent 包中。
- ConcurrentHashMap: 線程安全的HashMap
- CopyOnWriteArrayList: 線程安全的List,在讀多寫少的場合性能非常好,遠遠好于Vector.
- ConcurrentLinkedQueue: 高效的并發隊列,使用鏈表實現??梢钥醋鲆粋€線程安全的 LinkedList,這是一個非阻塞隊列。
- BlockingQueue: 這是一個接口,JDK內部通過鏈表、數組等方式實現了這個接口。表示阻塞隊列,非常適合用于作為數據共享的通道。
- ConcurrentSkipListMap: 跳表的實現。這是一個Map,使用跳表的數據結構進行快速查找。
二:ConcurrentHashMap
ConcurrentHashMap是Java并發包中提供的一個線程安全且高效的HashMap實現
1.分段鎖機制
概念由來:我們都知道HashTableHashTable線程安全的策略實現代價太大了,它將get/put所有相關操作都是使用synchronized修飾的,這相當于給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操作該對象,那其他線程只能阻塞,相當于將所有的操作串行化,在競爭激烈的并發場景中性能就會非常差。所以在jdk1.5~1.7版本提出了分段機制概念。
原理:分段鎖對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器里不同數據段的數據,就不會存在鎖競爭,提高并發訪問率。
結構圖
圖片來自:https://www.cnblogs.com/chengxiao/p/6842045.html
2.jdk1.8版本(現在所用的實現方式)
在jdk1.8版本中,由于采用分段鎖時,最大并發數受限于segment個數,有很大的局限性,所以在jdk1.8版本中ConcurrentHashMap取消了Segment分段鎖,采用CAS和synchronized來保證并發安全。數據結構跟HashMap1.8的結構類似,數組+鏈表/紅黑二叉樹。Java 8在鏈表長度超過一定閾值(8)時將鏈表(尋址時間復雜度為O(N))轉換為紅黑樹(尋址時間復雜度為O(log(N)))synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不沖突,就不會產生并發,性能得到了很大的提升。
JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 鏈表節點):
3.CAS原理
一般地,鎖分為悲觀鎖和樂觀鎖:悲觀鎖認為對于同一個數據的并發操作,一定是為發生修改的;而樂觀鎖則任務對于同一個數據的并發操作是不會發生修改的,在更新數據時會采用嘗試更新不斷重試的方式更新數據。
CAS(Compare And Swap,比較交換):CAS有三個操作數,內存值V、預期值A、要修改的新值B,當且僅當A和V相等時才會將V修改為B,否則什么都不做。Java中CAS操作通過JNI本地方法實現,在JVM中程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(Lock Cmpxchg);反之,如果程序是在單處理器上運行,就省略lock前綴。
Intel的手冊對lock前綴的說明如下:
確保對內存的讀-改-寫操作原子執行。之前采用鎖定總線的方式,但開銷很大;后來改用緩存鎖定來保證指令執行的原子性。
 ?? ?禁止該指令與之前和之后的讀和寫指令重排序。
 ?? ?把寫緩沖區中的所有數據刷新到內存中。
 CAS同時具有volatile讀和volatile寫的內存語義。
不過CAS操作也存在一些缺點:1. 存在ABA問題,其解決思路是使用版本號;2. 循環時間長,開銷大;3. 只能保證一個共享變量的原子操作。
三:CopyOnWriteArrayList
在很多應用場景中,讀操作可能會遠遠大于寫操作。由于讀操作根本不會修改原有的數據,因此對于每次讀取都進行加鎖其實是一種資源浪費。我們應該允許多個線程同時訪問List的內部數據,畢竟讀取操作是安全的。
CopyOnWriteArrayList 類的所有可變操作(add,set等等)都是通過創建底層數組的新副本來實現的。當 List 需要被修改的時候,我并不修改原有內容,而是對原有數據進行一次復制,將修改的內容寫入副本。寫完之后,再將修改完的副本替換原來的數據,這樣就可以保證寫操作不會影響讀操作了。
從 CopyOnWriteArrayList 的名字就能看出CopyOnWriteArrayList 是滿足CopyOnWrite 的ArrayList,所謂CopyOnWrite 也就是說:在計算機,如果你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之后呢,就將指向原來內存指針指向新的內存,原來的內存就可以被回收掉了。
四:ConcurrentLinkedQueue
Java提供的線程安全的 Queue 可以分為阻塞隊列和非阻塞隊列,其中阻塞隊列的典型例子是 BlockingQueue,非阻塞隊列的典型例子是ConcurrentLinkedQueue,在實際應用中要根據實際需要選用阻塞隊列或者非阻塞隊列。 阻塞隊列可以通過加鎖來實現,非阻塞隊列可以通過 CAS 操作實現。
從名字可以看出,ConcurrentLinkedQueue這個隊列使用鏈表作為其數據結構.ConcurrentLinkedQueue 應該算是在高并發環境中性能最好的隊列了。它之所有能有很好的性能,是因為其內部復雜的實現。
ConcurrentLinkedQueue 內部代碼我們就不分析了,大家知道ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法來實現線程安全就好了。
ConcurrentLinkedQueue 適合在對性能要求相對較高,同時對隊列的讀寫存在多個線程同時進行的場景,即如果對隊列加鎖的成本較高則適合使用無鎖的ConcurrentLinkedQueue來替代。
五:BlockingQueue
1.BlockingQueue簡介
上面我們己經提到了 ConcurrentLinkedQueue 作為高性能的非阻塞隊列。下面我們要講到的是阻塞隊列——BlockingQueue。阻塞隊列(BlockingQueue)被廣泛使用在“生產者-消費者”問題中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。當隊列容器已滿,生產者線程會被阻塞,直到隊列未滿;當隊列容器為空時,消費者線程會被阻塞,直至隊列非空時為止。
BlockingQueue 是一個接口,繼承自 Queue,所以其實現類也可以作為 Queue 的實現來使用,而 Queue 又繼承自 Collection 接口。下面是 BlockingQueue 的相關實現類:
下面主要介紹一下:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,這三個 BlockingQueue 的實現類。
2.ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 接口的有界隊列實現類,底層采用數組來實現。ArrayBlockingQueue一旦創建,容量不能改變。其并發控制采用可重入鎖來控制,不管是插入操作還是讀取操作,都需要獲取到鎖才能進行操作。當隊列容量滿時,嘗試將元素放入隊列將導致操作阻塞;嘗試從一個空隊列中取一個元素也會同樣阻塞。
ArrayBlockingQueue 默認情況下不能保證線程訪問隊列的公平性,所謂公平性是指嚴格按照線程等待的絕對時間順序,即最先等待的線程能夠最先訪問到 ArrayBlockingQueue。而非公平性則是指訪問 ArrayBlockingQueue 的順序不是遵守嚴格的時間順序,有可能存在,當 ArrayBlockingQueue 可以被訪問時,長時間阻塞的線程依然無法訪問到 ArrayBlockingQueue。
3.LinkedBlockingQueue
LinkedBlockingQueue 底層基于單向鏈表實現的阻塞隊列,可以當做無界隊列也可以當做有界隊列來使用,同樣滿足FIFO的特性,與ArrayBlockingQueue 相比起來具有更高的吞吐量,為了防止 LinkedBlockingQueue 容量迅速增,損耗大量內存。通常在創建LinkedBlockingQueue 對象時,會指定其大小,如果未指定,容量等于Integer.MAX_VALUE。
4.PriorityBlockingQueue
PriorityBlockingQueue 是一個支持優先級的無界阻塞隊列。默認情況下元素采用自然順序進行排序,也可以通過自定義類實現 compareTo() 方法來指定元素排序規則,或者初始化時通過構造器參數 Comparator 來指定排序規則。
PriorityBlockingQueue 并發控制采用的是 ReentrantLock,隊列為無界隊列(ArrayBlockingQueue 是有界隊列,LinkedBlockingQueue 也可以通過在構造函數中傳入 capacity 指定隊列最大的容量,但是 PriorityBlockingQueue 只能指定初始的隊列大小,后面插入元素的時候,如果空間不夠的話會自動擴容)。
簡單地說,它就是 PriorityQueue 的線程安全版本。不可以插入 null 值,同時,插入隊列的對象必須是可比較大小的(comparable),否則報 ClassCastException 異常。它的插入操作 put 方法不會 block,因為它是無界隊列(take 方法在隊列為空的時候會阻塞)。
六:ConcurrentSkipListMap
?
為了引出ConcurrentSkipListMap,先帶著大家簡單理解一下跳表。
對于一個單鏈表,即使鏈表是有序的,如果我們想要在其中查找某個數據,也只能從頭到尾遍歷鏈表,這樣效率自然就會很低,跳表就不一樣了。跳表是一種可以用來快速查找的數據結構,有點類似于平衡樹。它們都可以對元素進行快速的查找。但一個重要的區別是:對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整。而對跳表的插入和刪除只需要對整個數據結構的局部進行操作即可。這樣帶來的好處是:在高并發的情況下,你會需要一個全局鎖來保證整個平衡樹的線程安全。而對于跳表,你只需要部分鎖即可。這樣,在高并發環境下,你就可以擁有更好的性能。而就查詢的性能而言,跳表的時間復雜度也是 O(logn) 所以在并發數據結構中,JDK 使用跳表來實現一個 Map。
跳表的本質是同時維護了多個鏈表,并且鏈表是分層的,
最低層的鏈表維護了跳表內所有的元素,每上面一層鏈表都是下面一層的子集。
跳表內的所有鏈表的元素都是排序的。查找時,可以從頂級鏈表開始找。一旦發現被查找的元素大于當前鏈表中的取值,就會轉入下一層鏈表繼續找。這也就是說在查找過程中,搜索是跳躍式的。如上圖所示,在跳表中查找元素18。
查找18 的時候原來需要遍歷 18 次,現在只需要 7 次即可。針對鏈表長度比較大的時候,構建索引查找效率的提升就會非常明顯。
從上面很容易看出,跳表是一種利用空間換時間的算法。
使用跳表實現Map 和使用哈希算法實現Map的另外一個不同之處是:哈希并不會保存元素的順序,而跳表內所有的元素都是排序的。因此在對跳表進行遍歷時,你會得到一個有序的結果。所以,如果你的應用需要有序性,那么跳表就是你不二的選擇。JDK 中實現這一數據結構的類是ConcurrentSkipListMap。
大部分內容引用于:https://snailclimb.top/JavaGuide/#/java/Multithread/并發容器總結
總結
以上是生活随笔為你收集整理的Java集合之并发容器的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Java集合详解之Map
- 下一篇: volatile关键字之全面深度剖析
