ps:在下文中分別以Array代表ArrayBlockingQueue,Link代表LinkedBlockingQueue,下文中不再說明。
Array和Link在並發場景中經常使用,他們的共同作用就是實現線程安全隊列。下面對這兩種隊列的實現進行對比分析。
底層實現
ArrayBlockingQueue
- 底層基於數組實現,在對象創建時需要指定數組大小。在構建對象時,已經創建了數組。所以使用Array需要特別注意設定合適的隊列大小,如果設置過大會造成內存浪費。如果設置內存太小,就會影響並發的性能。
- 功能上,其內部維護了兩個索引指針putIndex和takeIndex。putIndex表示下次調用offer時存放元素的位置,takeIndex表示的時下次調用take時獲取的元素。有了這兩個索引的支持后,還是無法說明白其底層的實現原理。那么我們來看一段其內部出現最多的代碼:
int i = takeIndex;
...
if (++i == items.length)
i = 0;
...
這幾行在代碼在Array中幾乎每個函數都會用到。意思不管是在讀取元素,或者存放元素,如果到達數組的最后一個元素,直接將索引移動到第一個位置。你可能會想,如果我一直往隊列中添加元素而不取,添加的元素個數超過了數組長度,會不會覆蓋之前添加的元素。在實際使用過程中是不會出現這種情況的,其內部使用了ReentrantLock的Condition,這部分在並發支持中介紹。
LinkedBlockingQueue
- 底層基於單向鏈表實現。實現了隊列的功能,元素到來放到鏈表頭,從鏈表尾部取取數據。這種數據結構沒有必要使用雙向鏈表。鏈表的好處(數組的沒有的)是不用提前分配內存。Link也支持在創建對象時指定隊列長度,如果沒有指定,默認為Integer.MAX_VALUE。
並發支持
最大的區別就是Array內部只有一把鎖,offer和take使用同一把鎖,而Link的offer和take使用不同的鎖。
ReentrantLock和其Condition的關系
在做具體分析之前,先介紹一下ReentrantLock 和其Condition之間的關系。ReentrantLock內部維護了一個雙向鏈表,鏈表上的每個節點都會保存一個線程,鎖在雙向鏈表的頭部自選,取出線程執行。而Condition內部同樣維持着一個雙向鏈表,但是其向鏈表中添加元素(await)和從鏈表中移除(signal)元素沒有像ReentrantLock那樣,保證線程安全,所以在調用Condition的await()和signal()方法時,需要在lock.lock()和lock.unlock()之間以保證線程的安全。在調用Condition的signal時,它從自己的雙向鏈表中取出一個節點放到了ReentrantLock的雙向鏈表中,所以在具體的運行過程中不管ReentrantLock new 了幾個Condition其實內部公用的一把鎖。介紹完這個之后,我么來分析ArrayBlockingQueue和LinkedBlockingQueue的內部實現不同。
ArrayBlockingQueue
先看其內部鎖的定義:
int count;
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
- lock 其內部的鎖
- notEmpty 當調用offer時,會調用notEmpty.signal() 通知之前因為隊列空而被阻塞的線程。同時在take后,如果內部計數器count=0時,會調用notEmpty.await() 阻塞調用take的線程。
- notFull 當調用offer時,如果現在count=內部數組的長度時,會調用notFull.await()阻塞現在添加元素的所有線程;當調用take時,總會調用notFull.signal()喚醒之前因為隊列滿而阻塞的線程。
根據上面分析ReentrantLock和其Condition的關系,可以看到放元素和取元素用的同一把鎖,無法使放元素和取元素同時進行,只能先后相繼執行。
LinkedBlockingQueue
內部鎖定義:
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
- count 內部元素計數器使用的原子類型的計數器,使的元素個數的更新支持並發,為下面取和放元素並發提供了支持。
- takeLock 取元素單獨的鎖,和放元素分開,這樣即使有Condition也可以使的取和放元素在不同的節點上自選
- notEmpty 取元素的Condition鎖,和放元素鎖分開。
- putLock notFull 和上面介紹的takeLock notEmpty一直。
通過這種設置,可以將在鏈表頭上放元素和在鏈表尾部取元素不再競爭鎖,在一定程度上可以加快數據處理。