轉載至:https://www.jianshu.com/p/7b2f1fa616c6
- 前言
BlockingQueue即阻塞隊列,它是基於ReentrantLock,依據它的基本原理,我們可以實現Web中的長連接聊天功能,當然其最常用的還是用於實現生產者與消費者模式,大致如下圖所示:
在Java中,BlockingQueue是一個接口,它的實現類有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它們的區別主要體現在存儲結構上或對元素操作上的不同,但是對於take與put操作的原理,卻是類似的。
- 阻塞與非阻塞
入隊
offer(E e):如果隊列沒滿,立即返回true; 如果隊列滿了,立即返回false-->不阻塞
put(E e):如果隊列滿了,一直阻塞,直到隊列不滿了或者線程被中斷-->阻塞
offer(E e, long timeout, TimeUnit unit):在隊尾插入一個元素,,如果隊列已滿,則進入等待 -->阻塞 ,
直到出現以下三種情況:
被喚醒
等待時間超時
當前線程被中斷
出隊
poll():如果沒有元素,直接返回null;如果有元素,出隊
take():如果隊列空了,一直阻塞,直到隊列不爲空或者線程被中斷-->阻塞
poll(long timeout, TimeUnit unit):如果隊列不空,出隊;如果隊列已空且已經超時,返回null;如果隊列已空且時間未超時,則進入等待,
直到出現以下三種情況:
被喚醒
等待時間超時
當前線程被中斷
- LinkedBlockingQueue 源碼分析
LinkedBlockingQueue是一個基於鏈表實現的可選容量的阻塞隊列。隊頭的元素是插入時間最長的,隊尾的元素是最新插入的。新的元素將會被插入到隊列的尾部。
LinkedBlockingQueue的容量限制是可選的,如果在初始化時沒有指定容量,那麼默認使用int的最大值作爲隊列容量。
底層數據結構
LinkedBlockingQueue內部是使用鏈表實現一個隊列的,但是卻有別於一般的隊列,在於該隊列至少有一個節點,頭節點不含有元素。結構圖如下:
原理
LinkedBlockingQueue中維持兩把鎖,一把鎖用於入隊,一把鎖用於出隊,這也就意味着,同一時刻,只能有一個線程執行入隊,其餘執行入隊的線程將會被阻塞;同時,可以有另一個線程執行出隊,其餘執行出隊的線程將會被阻塞。換句話說,雖然入隊和出隊兩個操作同時均只能有一個線程操作,但是可以一個入隊線程和一個出隊線程共同執行,也就意味着可能同時有兩個線程在操作隊列,那麼爲了維持線程安全,LinkedBlockingQueue使用一個AtomicInterger類型的變量表示當前隊列中含有的元素個數,所以可以確保兩個線程之間操作底層隊列是線程安全的。
源碼分析
LinkedBlockingQueue可以指定容量,內部維持一個隊列,所以有一個頭節點head和一個尾節點last,內部維持兩把鎖,一個用於入隊,一個用於出隊,還有鎖關聯的Condition對象。主要對象的定義如下:
//容量,如果沒有指定,該值爲Integer.MAX_VALUE;
private final int capacity;
//當前隊列中的元素
private final AtomicInteger count =new AtomicInteger();
//隊列頭節點,始終滿足head.item==null
transient Node head;
//隊列的尾節點,始終滿足last.next==null
private transient Node last;
//用於出隊的鎖
private final ReentrantLock takeLock =new ReentrantLock();
//當隊列爲空時,保存執行出隊的線程
private final Condition notEmpty = takeLock.newCondition();
//用於入隊的鎖
private final ReentrantLock putLock =new ReentrantLock();
//當隊列滿時,保存執行入隊的線程
private final Condition notFull = putLock.newCondition();
put(E e)方法
put(E e)方法用於將一個元素插入到隊列的尾部,其實現如下:
public void put(E e)throws InterruptedException {
//不允許元素爲null
if (e ==null)throw new NullPointerException();
int c = -1;
//以當前元素新建一個節點
Node node =new Node(e);
final ReentrantLock putLock =this.putLock;
final AtomicInteger count =this.count;
//獲得入隊的鎖
putLock.lockInterruptibly();
try {
//如果隊列已滿,那麼將該線程加入到Condition的等待隊列中
while (count.get() == capacity) {
notFull.await();
}
//將節點入隊
enqueue(node);
//得到插入之前隊列的元素個數
c = count.getAndIncrement();
//如果還可以插入元素,那麼釋放等待的入隊線程
if (c +1 < capacity){
notFull.signal();
}
}finally {
//解鎖
putLock.unlock();
}
//通知出隊線程隊列非空
if (c ==0)signalNotEmpty();
}
3.1 具體入隊與出隊的原理圖:
圖中每一個節點前半部分表示封裝的數據x,後邊的表示指向的下一個引用。
初始化之後,初始化一個數據爲null,且head和last節點都是這個節點。
3.2、入隊兩個元素過後
3.3、出隊一個元素後
put方法總結:
-
LinkedBlockingQueue不允許元素爲null。
-
同一時刻,只能有一個線程執行入隊操作,因爲putLock在將元素插入到隊列尾部時加鎖了
-
如果隊列滿了,那麼將會調用notFull的await()方法將該線程加入到Condition等待隊列中。await()方法就會釋放線程佔有的鎖,這將導致之前由於被鎖阻塞的入隊線程將會獲取到鎖,執行到while循環處,不過可能因爲由於隊列仍舊是滿的,也被加入到條件隊列中。
-
一旦一個出隊線程取走了一個元素,並通知了入隊等待隊列中可以釋放線程了,那麼第一個加入到Condition隊列中的將會被釋放,那麼該線程將會重新獲得put鎖,繼而執行enqueue()方法,將節點插入到隊列的尾部
-
然後得到插入一個節點之前的元素個數,如果隊列中還有空間可以插入,那麼就通知notFull條件的等待隊列中的線程。
-
通知出隊線程隊列爲空了,因爲插入一個元素之前的個數爲0,而插入一個之後隊列中的元素就從無變成了有,就可以通知因隊列爲空而阻塞的出隊線程了。
E take()方法
take()方法用於得到隊頭的元素,在隊列爲空時會阻塞,知道隊列中有元素可取。其實現如下:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//獲取takeLock鎖
takeLock.lockInterruptibly();
try {
//如果隊列爲空,那麼加入到notEmpty條件的等待隊列中
while (count.get() == 0) {
notEmpty.await();
}
//得到隊頭元素
x = dequeue();
//得到取走一個元素之前隊列的元素個數
c = count.getAndDecrement();
//如果隊列中還有數據可取,釋放notEmpty條件等待隊列中的第一個線程
if (c > 1)notEmpty.signal();
} finally {
takeLock.unlock();
}
//如果隊列中的元素從滿到非滿,通知put線程
if (c == capacity)signalNotFull();
return x;
}
take方法總結:
當隊列爲空時,就加入到notEmpty(的條件等待隊列中,當隊列不爲空時就取走一個元素,當取完發現還有元素可取時,再通知一下自己的夥伴(等待在條件隊列中的線程);最後,如果隊列從滿到非滿,通知一下put線程。
remove()方法
remove()方法用於刪除隊列中一個元素,如果隊列中不含有該元素,那麼返回false;有的話則刪除並返回true。入隊和出隊都是隻獲取一個鎖,而remove()方法需要同時獲得兩把鎖,其實現如下:
public boolean remove(Object o) {
//因爲隊列不包含null元素,返回false
if (o == null) return false;
//獲取兩把鎖
fullyLock();
try {
//從頭的下一個節點開始遍歷
for (Node trail = head, p = trail.next;
p != null;trail = p, p = p.next) {
//如果匹配,那麼將節點從隊列中移除,trail表示前驅節點
if (o.equals(p.item)) {
unlink(p, trail);
return true;
}
}
return false;
} finally {
//釋放兩把鎖
fullyUnlock();
}
}
void fullyLock() {
putLock.lock();
takeLock.lock();
}
提問:爲什麼remove()方法同時需要兩把鎖?
LinkedBlockingQueue總結:
LinkedBlockingQueue是允許兩個線程同時在兩端進行入隊或出隊的操作的,但一端同時只能有一個線程進行操作,這是通過兩把鎖來區分的;
爲了維持底部數據的統一,引入了AtomicInteger的一個count變量,表示隊列中元素的個數。count只能在兩個地方變化,一個是入隊的方法(可以+1),另一個是出隊的方法(可以-1),而AtomicInteger是原子安全的,所以也就確保了底層隊列的數據同步。
4. ArrayBlockingQueue源碼分析
ArrayBlockingQueue底層是使用一個數組實現隊列的,並且在構造ArrayBlockingQueue時需要指定容量,也就意味着底層數組一旦創建了,容量就不能改變了,因此ArrayBlockingQueue是一個容量限制的阻塞隊列。因此,在隊列全滿時執行入隊將會阻塞,在隊列爲空時出隊同樣將會阻塞。
ArrayBlockingQueue的重要字段有如下幾個:
/** The queued items */
final Object[] items;
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
put(E e)方法
put(E e)方法在隊列不滿的情況下,將會將元素添加到隊列尾部,如果隊列已滿,將會阻塞,直到隊列中有剩餘空間可以插入。該方法的實現如下:
public void put(E e) throws InterruptedException {
//檢查元素是否爲null,如果是,拋出NullPointerException
checkNotNull(e);
final ReentrantLock lock = this.lock;
//加鎖
lock.lockInterruptibly();
try {
//如果隊列已滿,阻塞,等待隊列成爲不滿狀態
while (count == items.length)
notFull.await();
//將元素入隊
enqueue(e);
} finally {
lock.unlock();
}
}
put方法總結:
-
ArrayBlockingQueue不允許元素爲null
-
ArrayBlockingQueue在隊列已滿時將會調用notFull的await()方法釋放鎖並處於阻塞狀態
-
一旦ArrayBlockingQueue不爲滿的狀態,就將元素入隊
E take()方法
take()方法用於取走隊頭的元素,當隊列爲空時將會阻塞,直到隊列中有元素可取走時將會被釋放。其實現如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//首先加鎖
lock.lockInterruptibly();
try {
//如果隊列爲空,阻塞
while (count == 0)
notEmpty.await();
//隊列不爲空,調用dequeue()出隊
return dequeue();
} finally {
//釋放鎖
lock.unlock();
}
}
take方法總結:
一旦獲得了鎖之後,如果隊列爲空,那麼將阻塞;否則調用dequeue()出隊一個元素。
ArrayBlockingQueue總結:
ArrayBlockingQueue的併發阻塞是通過ReentrantLock和Condition來實現的,ArrayBlockingQueue內部只有一把鎖,意味着同一時刻只有一個線程能進行入隊或者出隊的操作。
5 總結
在上面分析LinkedBlockingQueue的源碼之後,可以與ArrayBlockingQueue做一個比較。
ArrayBlockingQueue:
一個對象數組+一把鎖+兩個條件
入隊與出隊都用同一把鎖
在只有入隊高併發或出隊高併發的情況下,因爲操作數組,且不需要擴容,性能很高
採用了數組,必須指定大小,即容量有限
LinkedBlockingQueue:
一個單向鏈表+兩把鎖+兩個條件
兩把鎖,一把用於入隊,一把用於出隊,有效的避免了入隊與出隊時使用一把鎖帶來的競爭。
在入隊與出隊都高併發的情況下,性能比ArrayBlockingQueue高很多
採用了鏈表,最大容量爲整數最大值,可看做容量無限