一、簡介
所謂阻塞隊列,其實就是支持下面這兩種阻塞功能的隊列:
- 當隊列爲空時,讀取該隊列可以阻塞直到隊列不爲空;
- 當隊列已滿時,寫入該隊列可以阻塞直到隊列不爲滿;
這種阻塞隊列主要用於可以用來構建生產者-消費者模型,生產者只需要往隊列中發送消息,而消費者也只需要專注於從隊列中讀取消息,剩下的同步、阻塞細節都交給阻塞隊列把。
Java提供了下面7種阻塞隊列,區別於底層數據結構的不同:
- ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
- LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
- PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
- DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
- SynchronousQueue:一個不存儲元素的阻塞隊列。
- LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
- LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
立即返回結果值 | 超時返回結果值 | 阻塞 | 拋出異常 | |
插入 | offer(e) | offer(e,time,unit) | put(e) | add(e) |
移除 | poll() | poll(time,unit) | take() | remove() |
讀取 | peek() | 無 | 無 | element() |
二、源碼
-
ArrayBlockingQueue
下面以ArrayBlockingQueue爲例看看JDK的源碼,其他的實現類,有先看一下它的主要屬性:public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable{ /** 底層用於存放隊列元素的數組 */ final Object[] items; /** 下一個獲取索引,take,poll,peek和remove會用到 */ int takeIndex; /** 下一個插入索引,put,offer和add方法會用到 */ int putIndex; /** 當前隊列中元素的個數 */ int count; /** 用於控制併發操作隊列的鎖對象 */ final ReentrantLock lock; /** 隊列爲非空的條件對象,用於喚醒阻塞中的讀操作 */ private final Condition notEmpty; /** 隊列爲非滿的條件對象,用於喚醒阻塞中的寫操作 */ private final Condition notFull; }
-
寫入
1. offer(e)與offer(e,time,unit)
首先是offer(e),邏輯比較簡單,上鎖、判斷插入(不滿則插入,滿了則返回)、解鎖。public boolean offer(E e) { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; //鎖住隊列,防止在插入過程中的併發讀寫 lock.lock(); try { //滿了,返回false if (count == items.length) return false; else { //執行插入 insert(e); return true; } } finally { //釋放鎖 lock.unlock(); } }
而offer(e,time,unit)相比offer(e)則多了阻塞給定時間的功能,注意下面的無條件for循環是爲了防止多個線程同時被喚醒操作隊列,因此每次都需要判斷隊列是否已滿,是的話繼續阻塞:public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { if (e == null) throw new NullPointerException(); long nanos = unit.toNanos(timeout); //使用lockInterruptibly鎖住隊列,可以被中斷 final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { //如果隊列不滿,則執行插入並返回true if (count != items.length) { insert(e); return true; } //如果nanos<=0,說明已經阻塞超過了給定時間了,直接返回false if (nanos <= 0) return false; try { //若該條件沒被喚醒或者該線程沒被中斷,等待給定時間 //如果等待中被喚醒,返回剩餘的等待時間 nanos = notFull.awaitNanos(nanos); } catch (InterruptedException ie) { notFull.signal(); // propagate to non-interrupted thread throw ie; } } } finally { //釋放鎖 lock.unlock(); } }
看到兩個方法都是調用的insert(e)執行實際的插入,insert方法也比較簡單,插入、非空喚醒private void insert(E x) { items[putIndex] = x; putIndex = inc(putIndex); ++count; notEmpty.signal(); }
2.add(e)
add方法其實是調用了父類AbstractQueue的add方法:
很簡單,其實就是通過offer判斷當前隊列是否滿了,是就立刻拋出異常。public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
3. put(e) 其實put就相當與無限阻塞的offer(e,time,unit),也是在無限循環裏面判斷隊列是否已滿並插入,否則阻塞:public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); final E[] items = this.items; final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { try { //每次被喚醒都判斷一下隊列是否滿了,是則繼續阻塞 while (count == items.length) notFull.await(); } catch (InterruptedException ie) { notFull.signal(); // propagate to non-interrupted thread throw ie; } insert(e); } finally { lock.unlock(); } }
-
移除
讀取操作和寫入其實大同小異,底層和核心邏輯都差不多,如果理解了上面關於讀取的幾個方法的話,讀取就無需過多解釋了。
1.poll()與poll(time,unit)public E poll() { final ReentrantLock lock = this.lock; lock.lock(); try { if (count == 0) return null; E x = extract(); return x; } finally { lock.unlock(); } } public E poll(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { //無限循環,確保每次喚醒都要判斷當前是否爲空 for (; ; ) { if (count != 0) { E x = extract(); return x; } if (nanos <= 0) return null; try { nanos = notEmpty.awaitNanos(nanos); } catch (InterruptedException ie) { notEmpty.signal(); // propagate to non-interrupted thread throw ie; } } } finally { lock.unlock(); } } private E extract() { //讀取隊列頭元素並喚醒都有等待非滿線程 final E[] items = this.items; E x = items[takeIndex]; items[takeIndex] = null; takeIndex = inc(takeIndex); --count; notFull.signal(); return x; }
2.take()public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { try { //爲空的情況下,無限阻塞 while (count == 0) notEmpty.await(); } catch (InterruptedException ie) { notEmpty.signal(); // propagate to non-interrupted thread throw ie; } E x = extract(); return x; } finally { lock.unlock(); } }
3.remove()public boolean remove(Object o) { if (o == null) return false; final E[] items = this.items; final ReentrantLock lock = this.lock; lock.lock(); try { //遍歷隊列中的元素,使用equals方法判定相等則移除 int i = takeIndex; int k = 0; for (;;) { if (k++ >= count) return false; if (o.equals(items[i])) { removeAt(i); return true; } i = inc(i); } } finally { lock.unlock(); } }
-
讀取
首先說明一下,這下面的讀取方法都是指讀取隊列頭部的元素,因爲如果構建消息隊列,都是尾部插入,頭部讀取。讀取的兩個方法的核心邏輯其實都在peek()裏:上鎖 - 讀取 - 解鎖:public E element() { E x = peek(); if (x != null) return x; else throw new NoSuchElementException(); } public E peek() { final ReentrantLock lock = this.lock; lock.lock(); try { //takeIndex會一直指向隊列的頭 return (count == 0) ? null : items[takeIndex]; } finally { lock.unlock(); } }