Java阻塞隊列的原理分析
先看看BlockingQueue
接口的文檔說明:
- add:添加元素到隊列裏,添加成功返回true,由於容量滿了添加失敗會拋出
IllegalStateException
異常; - offer:添加元素到隊列裏,添加成功返回true,添加失敗返回false;
- put:添加元素到隊列裏,如果容量滿了會阻塞直到容量不滿;
- poll:刪除隊列頭部元素,如果隊列爲空,返回null。否則返回元素;
- remove:基於對象找到對應的元素,並刪除。刪除成功返回true,否則返回false;
- take:刪除隊列頭部元素,如果隊列爲空,一直阻塞到隊列有元素並刪除。
先看一個簡單的ArrayBlockingQueue
,ArrayBlockingQueue的原理就是使用一個可重入鎖和這個鎖生成的兩個條件對象進行併發控制(classic two-condition algorithm)。
ArrayBlockingQueue
ArrayBlockingQueue是一個帶有長度的阻塞隊列,初始化的時候必須要指定隊列長度,且指定長度之後不允許進行修改。
屬性如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** The queued items item的集合 */ final Object[] items; /** items index for next take, poll, peek or remove 拿數據的索引 */ int takeIndex; /** items index for next put, offer, or add 放數據的索引 */ int putIndex; /** Number of elements in the queue 隊列元素的個數 */ int count; /** Main lock guarding all access 可重入的鎖 */ final ReentrantLock lock; /** Condition for waiting takes 條件對象 */ private final Condition notEmpty; /** Condition for waiting puts 條件對象 */ private final Condition notFull; |
先看一下add方法:
1 2 3 4 5 6 |
public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); } |
offer方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lock(); try { if (count == items.length) return false; else { insert(e); return true; } } finally { lock.unlock(); } } |
我們可以看到,如果滿了返回false,如果沒有滿調用insert。整個方法是通過可重入鎖來鎖住的,並且最終釋放。接着看一下insert
方法:
1 2 3 4 5 6 |
private void insert(E x) { items[putIndex] = x; // 元素添加到數組裏 putIndex = inc(putIndex); // 放數據索引+1,當索引滿了變成0 ++count; // 元素個數+1 notEmpty.signal(); // 使用條件對象notEmpty通知 } |
這裏insert
被調用的時候就會喚醒notEmpty
上等待的線程進行take
操作。
再看一下put
方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
public void put(E e) throws InterruptedException { checkNotNull(e); // 不允許元素爲空 final ReentrantLock lock = this.lock; lock.lockInterruptibly(); // 加鎖,保證調用put方法的時候只有1個線程 try { while (count == items.length) // 如果隊列滿了,阻塞當前線程,while用來防止假喚醒 notFull.await(); // 線程阻塞並被掛起,同時釋放鎖 insert(e); // 調用insert方法 } finally { lock.unlock(); // 釋放鎖,讓其他線程可以調用put方法 } } |
add方法和offer方法不會阻塞線程,put方法如果隊列滿了會阻塞線程,直到有線程消費了隊列裏的數據纔有可能被喚醒。
繼續看刪除數據的相關操作,先看一下poll:
1 2 3 4 5 6 7 8 9 |
public E poll() { final ReentrantLock lock = this.lock; lock.lock(); // 加鎖,保證調用poll方法的時候只有1個線程 try { return (count == 0) ? null : extract(); // 如果隊列裏沒元素了,返回null,否則調用extract方法 } finally { lock.unlock(); // 釋放鎖,讓其他線程可以調用poll方法 } } |
看看這個extract
方法(jdk源碼的作者的起名水平真的非常高,代碼素質好):
1 2 3 4 5 6 7 8 9 |
private E extract() { final Object[] items = this.items; E x = this.<E>cast(items[takeIndex]); // 得到取索引位置上的元素 items[takeIndex] = null; // 對應取索引上的數據清空 takeIndex = inc(takeIndex); // 取數據索引+1,當索引滿了變成0 --count; // 元素個數-1 notFull.signal(); // 使用條件對象notFull通知,原理同上面的insert中 return x; // 返回元素 } |
看一下take
方法:
1 2 3 4 5 6 7 8 9 10 11 |
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); // 加鎖,保證調用take方法的時候只有1個線程 try { while (count == 0) // 如果隊列空,阻塞當前線程,並加入到條件對象notEmpty的等待隊列裏 notEmpty.await(); // 線程阻塞並被掛起,同時釋放鎖 return extract(); // 調用extract方法 } finally { lock.unlock(); // 釋放鎖,讓其他線程可以調用take方法 } } |
再看一下remove
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public boolean remove(Object o) { if (o == null) return false; final Object[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); // 加鎖,保證調用remove方法的時候只有1個線程 try { for (int i = takeIndex, k = count; k > 0; i = inc(i), k--) { // 遍歷元素 if (o.equals(items[i])) { // 兩個對象相等的話 removeAt(i); // 調用removeAt方法 return true; // 刪除成功,返回true } } return false; // 刪除成功,返回false } finally { lock.unlock(); // 釋放鎖,讓其他線程可以調用remove方法 } } |
再看一下removeAt
方法,這個方法反而比較有價值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private void removeAt(int i) { final Object[] items = this.items; if (i == takeIndex) { // 如果要刪除數據的索引是取索引位置,直接刪除取索引位置上的數據,然後取索引+1即可 items[takeIndex] = null; takeIndex = inc(takeIndex); } else { // 如果要刪除數據的索引不是取索引位置,移動元素元素,更新取索引和放索引的值 for (;;) { int nexti = inc(i); if (nexti != putIndex) { items[i] = items[nexti]; i = nexti; } else { items[i] = null; putIndex = i; break; } } } --count; // 元素個數-1 notFull.signal(); // 使用條件對象notFull通知 } |
LinkedBlockingQueue
LinkedBlockingQueue
是一個使用鏈表完成隊列操作的阻塞隊列。鏈表是單向鏈表,而不是雙向鏈表。
看一下屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/** The capacity bound, or Integer.MAX_VALUE if none 容量大小 */ private final int capacity; /** Current number of elements 元素個數,因爲有2個鎖,存在競態條件,使用AtomicInteger */ private final AtomicInteger count = new AtomicInteger(0); /** * Head of linked list. * Invariant: head.item == null * 頭結點 */ private transient Node<E> head; /** * Tail of linked list. * Invariant: last.next == null * 尾節點 */ private transient Node<E> last; /** 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(); |
ArrayBlockingQueue
只有1個鎖,添加數據和刪除數據的時候只能有1個被執行,不允許並行執行。
而LinkedBlockingQueue
有2個鎖,放元素鎖和取元素鎖,添加數據和刪除數據是可以並行進行的,當然添加數據和刪除數據的時候只能有1個線程各自執行。
add
方法內部調用offer
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public boolean offer(E e) { if (e == null) throw new NullPointerException(); // 不允許空元素 final AtomicInteger count = this.count; if (count.get() == capacity) // 如果容量滿了,返回false return false; int c = -1; Node<E> node = new Node(e); // 容量沒滿,以新元素構造節點 final ReentrantLock putLock = this.putLock; putLock.lock(); // 放鎖加鎖,保證調用offer方法的時候只有1個線程 try { // 再次判斷容量是否已滿,因爲可能取元素鎖在進行消費數據,沒滿的話繼續執行 if (count.get() < capacity) { enqueue(node); // 節點添加到鏈表尾部 c = count.getAndIncrement(); // 元素個數+1 if (c + 1 < capacity) // 如果容量還沒滿 notFull.signal(); // 在放鎖的條件對象notFull上喚醒正在等待的線程,表示可以再次往隊列裏面加數據 } } finally { putLock.unlock(); // 釋放放鎖,讓其他線程可以調用offer方法 } // 由於存在放元素鎖和取元素鎖,這裏可能取元素鎖一直在消費數據,count會變化。這裏的if條件表示如果隊列中還有1條數據 if (c == 0) // 在拿鎖的條件對象notEmpty上喚醒正在等待的1個線程,表示隊列裏還有1條數據,可以進行消費 signalNotEmpty(); return c >= 0; // 添加成功返回true,否則返回false } |
put
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // 不允許空元素 int c = -1; Node<E> node = new Node(e); // 以新元素構造節點 final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); // 放鎖加鎖,保證調用put方法的時候只有1個線程 try { while (count.get() == capacity) { // 如果容量滿了 notFull.await(); // 阻塞並掛起當前線程 } enqueue(node); // 節點添加到鏈表尾部 c = count.getAndIncrement(); // 元素個數+1 if (c + 1 < capacity) // 如果容量還沒滿 // 在放鎖的條件對象notFull上喚醒正在等待的線程,表示可以再次往隊列裏面加數據了,隊列還沒滿 notFull.signal(); } finally { putLock.unlock(); // 釋放放鎖,讓其他線程可以調用put方法 } // 由於存在放鎖和拿鎖,這裏可能拿鎖一直在消費數據,count會變化。這裏的if條件表示如果隊列中還有1條數據 if (c == 0) // 在拿鎖的條件對象notEmpty上喚醒正在等待的1個線程,表示隊列裏還有1條數據,可以進行消費 signalNotEmpty(); } |
ArrayBlockingQueue
中放入數據阻塞的時候,需要消費數據才能喚醒。
而LinkedBlockingQueue
中放入數據阻塞的時候,因爲它內部有2個鎖,可以並行執行放入數據和消費數據,不僅在消費數據的時候進行喚醒插入阻塞的線程,同時在插入的時候如果容量還沒滿,也會喚醒插入阻塞的線程。
poll
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public E poll() { final AtomicInteger count = this.count; if (count.get() == 0) // 如果元素個數爲0 return null; // 返回null E x = null; int c = -1; final ReentrantLock takeLock = this.takeLock; takeLock.lock(); // 拿鎖加鎖,保證調用poll方法的時候只有1個線程 try { if (count.get() > 0) { // 判斷隊列裏是否還有數據 x = dequeue(); // 刪除頭結點 c = count.getAndDecrement(); // 元素個數-1 if (c > 1) // 如果隊列裏還有元素 // 在拿鎖的條件對象notEmpty上喚醒正在等待的線程,表示隊列裏還有數據,可以再次消費 notEmpty.signal(); } } finally { takeLock.unlock(); // 釋放拿鎖,讓其他線程可以調用poll方法 } // 由於存在放鎖和拿鎖,這裏可能放鎖一直在添加數據,count會變化。這裏的if條件表示如果隊列中還可以再插入數據 if (c == capacity) // 在放鎖的條件對象notFull上喚醒正在等待的1個線程,表示隊列裏還能再次添加數據 signalNotFull(); return x; } |
take
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); // 拿鎖加鎖,保證調用take方法的時候只有1個線程 try { while (count.get() == 0) { // 如果隊列裏已經沒有元素了 notEmpty.await(); // 阻塞並掛起當前線程 } x = dequeue(); // 刪除頭結點 c = count.getAndDecrement(); // 元素個數-1 if (c > 1) // 如果隊列裏還有元素 // 在拿鎖的條件對象notEmpty上喚醒正在等待的線程,表示隊列裏還有數據,可以再次消費 notEmpty.signal(); } finally { takeLock.unlock(); // 釋放拿鎖,讓其他線程可以調用take方法 } // 由於存在放鎖和拿鎖,這裏可能放鎖一直在添加數據,count會變化。這裏的if條件表示如果隊列中還可以再插入數據 if (c == capacity) // 在放鎖的條件對象notFull上喚醒正在等待的1個線程,表示隊列裏還能再次添加數據 signalNotFull(); return x; } |
remove
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public boolean remove(Object o) { if (o == null) return false; fullyLock(); // remove操作要移動的位置不固定,2個鎖都需要加鎖 try { for (Node<E> trail = head, p = trail.next; // 從鏈表頭結點開始遍歷 p != null; trail = p, p = p.next) { if (o.equals(p.item)) { // 判斷是否找到對象 unlink(p, trail); // 修改節點的鏈接信息,同時調用notFull的signal方法 return true; } } return false; } finally { fullyUnlock(); // 2個鎖解鎖 } } |
LinkedBlockingQueue
的take方法對於沒數據的情況下會阻塞,poll方法刪除鏈表頭結點,remove方法刪除指定的對象。
需要注意的是remove方法由於要刪除的數據的位置不確定,需要2個鎖同時加鎖。