1. 程式人生 > >Java併發容器詳細介紹

Java併發容器詳細介紹

概述

        java.util包中的大部分容器都是非執行緒安全的,若要在多執行緒中使用容器,你可以使用Collections提供的包裝函式:synchronizedXXX,將普通容器變成執行緒安全的容器。但該方法僅僅是簡單地給容器使用同步,效率很低。因此併發大師Doug Lea提供了java.util.concurrent包,提供高效的併發容器。並且為了保持與普通的容器的介面一致性,仍然使用util包的介面,從而易於使用、易於理解。

PS:問題:synchronizedXXX究竟對容器做了什麼從而能達到執行緒安全的目的?


類圖

 1.png

List和Set

        JUC包中List介面的實現類:CopyOnWriteArrayList

    • CopyOnWriteArrayList是執行緒安全的ArrayList

  • JUC包中Set介面的實現類:CopyOnWriteArraySet、ConcurrentSkipListSet

    • CopyOnWriteArraySet是執行緒安全的Set,它內部包含了一個CopyOnWriteArrayList,因此本質上是由CopyOnWriteArrayList實現的。

    • ConcurrentSkipListSet相當於執行緒安全的TreeSet。它是有序的Set。它由ConcurrentSkipListMap實現。

Map

2.png

  • ConcurrentHashMap:執行緒安全的HashMap。採用分段鎖實現高效併發。

  • ConcurrentSkipListMap:執行緒安全的有序Map。使用跳錶實現高效併發。

Queue

3.png

  • ConcurrentLinkedQueue:執行緒安全的無界佇列。底層採用單鏈表。支援FIFO。

  • ConcurrentLinkedDeque:執行緒安全的無界雙端佇列。底層採用雙向連結串列。支援FIFO和FILO。

  • ArrayBlockingQueue:陣列實現的阻塞佇列。

  • LinkedBlockingQueue:連結串列實現的阻塞佇列。

  • LinkedBlockingDeque:雙向連結串列實現的雙端阻塞佇列。


 CopyOnWrite容器(寫時複製容器)

CopyOnWrite容器包括:CopyOnWriteArrayList和CopyOnWriteArraySet。

  • PS:CopyOnWriteArraySet有CopyOnWriteArrayList實現。

特性

  • 適用於讀操作遠遠多於寫操作,並且資料量較小的情況。

  • 修改容器的代價是昂貴的,因此建議批量增加addAll、批量刪除removeAll。

CopyOnWrite容器是如何實現執行緒安全的?

  1. 使用volatile修飾陣列引用:確保陣列引用的記憶體可見性。

  2. 對容器修改操作進行同步:從而確保同一時刻只能有一條執行緒修改容器(因為修改容器都會產生一個新的容器,增加同步可避免同一時刻複製生成多個容器,從而無法保證陣列資料的一致性)

  3. 修改時複製容器:確保所有修改操作都作用在新陣列上,原本的陣列在建立過後就用不變化,從而其他執行緒可以放心地讀。

 

新增方法

CopyOnWriteArrayList:

// 新增集合中不存在的元素
int addAllAbsent(Collection<? extends E> c)
// 該元素若不存在則新增
boolean addIfAbsent(E e)

CopyOnWriteArraySet:木有新增!

 

迭代

  • CopyOnWriteArrayList擁有內部類:COWIterator,它是ListIterator的子類。

  • 當呼叫iterator函式時返回的是COWIterator物件。

  • COWIterator不允許修改容器,你若呼叫則會丟擲UnsupportedOperationException。

 

優點

讀操作無需加鎖,從而高效。

 

缺點

  • 資料一致性問題

    • 由於迭代的是容器當前的快照,因此在迭代過程中容器發生的修改並不能實時被當前正在迭代的執行緒感知。

  • 記憶體佔用問題

    • 由於修改容器都會複製陣列,從而當陣列超大時修改容器效率很低。

    • PS:因此寫時複製容器適合儲存小容量資料。

 


 

ConcurrentHashMap

 

java.util包中提供了執行緒安全的HashTable,但這傢伙只是通過簡單的同步來實現執行緒安全,因此效率低。只要有一條執行緒獲取了容器的鎖之後,其他所有的執行緒訪問同步函式都會被阻塞。因此同一時刻只能有一條執行緒訪問同步函式。而ConcurrentHashMap採用了分段鎖機制實現高效的併發訪問。

 

分段鎖原理

ConcurrentHashMap由多個Segment構成,每個Segment都包含一張雜湊表。每次操作只將操作資料所屬的Segment鎖起來,從而避免將整個鎖住。

 

資料結構

4.png

  • ConcurrentHashMap內部包含了Segment陣列,而每個Segment又繼承自ReentrantLock,因此它是一把可重入的鎖。

  • Segment內部擁有一個HashEntry陣列,它就是一張雜湊表。HashEntry是單鏈表的一個節點,HashEntry陣列儲存單鏈表的表頭節點。

 

新增API

V putIfAbsent(K key, V value)

 


 

ConcurrentSkipListMap

 

  • 它是一個有序的Map,相當於TreeMap。

  • TreeMap採用紅黑樹實現排序,而ConcurrentHashMap採用跳錶實現有序。

 

跳錶的由來

作用:儲存有序序列,並且實現高效的查詢與插入刪除。

儲存有序序列最簡單的辦法就是使用陣列,從而查詢可以採用二分搜尋,但插入刪除需要移動元素較為低效。

因此出現了二叉搜尋樹,用來解決插入刪除移動元素的問題。但二叉搜尋樹在最壞情況下會退化成一條單鏈表,搜尋的效率降為O(n)。

為了避免二叉搜尋樹的退化,出現了二叉平衡樹,它在每次插入刪除節點後都會重新調整樹形,使得它仍然保持平衡,從而保證了搜尋效率,也保證了插入刪除的效率。

此外,根據平衡演算法的不同,二叉平衡樹又分為:B+樹、B-樹、紅黑樹。

但平衡演算法過於複雜,因此出現跳錶。

 

跳錶介紹

跳錶是條有序的單鏈表,它的每個節點都有多個指向後繼節點的引用。

它有多個層次,上層都是下層的子集,從而能跳過不必要的節點,提升搜尋速度。

它通過空間來換取時間。

如查詢19的過程:

5.png

 


 

ConcurrentSkipListSet

  • 它是一個有序的、執行緒安全的Set,相當於執行緒安全的TreeSet。

  • 它內部擁有ConcurrentSkipListMap例項,本質上就是一個ConcurrentSkipListMap,只不過僅使用了Map中的key。

 


 

ArrayBlockingQueue

 

概要

  • ArrayBlockingQueue是一個 陣列實現的 執行緒安全的 有限 阻塞佇列。

 

資料結構

6.png

  • ArrayBlockingQueue繼承自AbstractQueue,並實現了BlockingQueue介面。

  • ArrayBlockingQueue內部由Object陣列儲存元素,構造時必須要指定佇列容量。

  • ArrayBlockingQueue由ReentrantLock實現佇列的互斥訪問,並由notEmpty、notFull這兩個Condition分別實現隊空、隊滿的阻塞。

  • ReentrantLock分為公平鎖和非公平鎖,可以在構造ArrayBlockingQueue時指定。預設為非公平鎖。

 

新增API

// 在隊尾新增指定元素,若隊已滿則等待指定時間
boolean offer(E e, long timeout, TimeUnit unit)
// 獲取並刪除隊首元素,若隊為空則阻塞等待
E take()
// 新增指定元素,若隊已滿則一直等待
 
void put(E e)
// 獲取隊首元素,若隊為空,則等待指定時間
E poll(long timeout, TimeUnit unit)

 

隊滿、隊空阻塞喚醒的原理

  • 隊滿阻塞:當新增元素時,若隊滿,則呼叫notFull.await()阻塞當前執行緒;當移除一個元素時呼叫notFull.signal()喚醒在notFull上等待的執行緒。

  • 隊空阻塞:當刪除元素時,若隊為空,則呼叫notEmpty.await()阻塞當前執行緒;當隊首新增元素時,呼叫notEmpty.signal()喚醒在notEmpty上等待的執行緒。

 


 

LinkedBlockingQueue

 

概要

  • LinkedBlockingQueue是一個 單鏈表實現的、執行緒安全的、無限 阻塞佇列。

 

資料結構

7.png

  • LinkedBlockingQueue繼承自AbstractQueue,實現了BlockingQueue介面。

  • LinkedBlockingQueue由單鏈表實現,因此是個無限佇列。但為了方式無限膨脹,構造時可以加上容量加以限制。

  • LinkedBlockingQueue分別採用讀取鎖和插入鎖控制讀取/刪除 和 插入過程的併發訪問,並採用notEmpty和notFull兩個Condition實現隊滿隊空的阻塞與喚醒。

 

隊滿隊空阻塞喚醒的原理

  • 隊滿阻塞:若要插入元素,首先需要獲取putLock;在此基礎上,若此時隊滿,則呼叫notFull.await(),阻塞當前執行緒;當移除一個元素後呼叫notFull.signal()喚醒在notFull上等待的執行緒;最後,當插入操作完成後釋放putLock。

  • 隊空阻塞:若要刪除/獲取元素,首先要獲取takeLock;在此基礎上,若隊為空,則呼叫notEmpty.await(),阻塞當前執行緒;當插入一個元素後呼叫notEmpty.signal()喚醒在notEmpty上等待的執行緒;最後,當刪除操作完成後釋放takeLock。

 

PS:API和ArrayBlockingQueue一樣。

 


 

LinkedBlockingDeque

 

概要

  • 它是一個 由雙向連結串列實現的、執行緒安全的、 雙端 無限 阻塞佇列。

 

資料結構

8.png

 


 

ConcurrentLinkedQueue
 

概述

  • 它是一個由單鏈表實現的、執行緒安全的、無限 佇列。

 

資料結構

9.png

  • 它僅僅繼承了AbstractQueue,並未實現BlockingQueue介面,因此它不是阻塞佇列,僅僅是個執行緒安全的普通佇列。

特性

  • head、tail、next、item均使用volatile修飾,保證其記憶體可見性,並未使用鎖,從而提高併發效率。

  • PS:它究竟是怎樣在不使用鎖的情況下實現執行緒安全的?

  • PS:關注360linker公眾號,入官方社群取免費視訊教程、知名單位招聘資訊。交流分享IT圈學習經驗。