這篇文章來說說稍微復雜一些的LinkedBlockingQueue。LinkedBlockingQueue使用一個鏈表來實現,會有一個head和tail分別指向隊列的開始和隊列的結尾。因此LinkedBlockingQueue會有兩把鎖,分別控制這兩個元素,這樣在添加元素和拿走元素的時候就不會有鎖的沖突,因此取走元素操作的是head,而添加元素操作的是tail。
老規矩先看offer方法和poll方法
public boolean offer(E e) { if (e == null) throw new NullPointerException(); final AtomicInteger count = this.count; if (count.get() == capacity) return false; int c = -1; Node<E> node = new Node(e); final ReentrantLock putLock = this.putLock; putLock.lock(); try { if (count.get() < capacity) { enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); return c >= 0; }
可以看到offer方法在添加元素時候僅僅涉及到putLock,但是還是會需要takeLock,看看signalNotEmpty代碼就知道。而poll方法拿走元素的時候涉及到takeLock,也是會需要putLock。參見signalNotFull()。關于signalNotEmpty會在后面講阻塞的時候講到。
public E poll() { final AtomicInteger count = this.count; if (count.get() == 0) return null; E x = null; int c = -1; final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { if (count.get() > 0) { x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
這里順便說說隊列長度的count,因為有兩把鎖存在,所以如果還是像ArrayBlockingQueue一樣使用基本類型的count的話會同時用到兩把鎖,這樣就會很復雜,因此直接使用原子數據類型AtomicInteger來操作count。
接下來談談阻塞的問題,一個BlockingQueue會有兩個Condition:notFull和notEmpty,LinkedBlockingQueue會有兩把鎖,因此這兩個Condition肯定是由這兩個鎖分別創建的,takeLock創建notEmpty,putLock創建notFull。
/** 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();
接下來看看put方法:
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ while (count.get() == capacity) { notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); }
其實大體邏輯和ArrayBlockingQueue差不多,也會需要通知notEmpty條件,因為notEmpty條件屬于takeLock,而調用signal方法需要獲取Lock,因此put方法也是用到了另外一個鎖:takeLock。這里有一點會不同,按照道理來說put方法是不需要通知notFull條件的,是由由拿走元素的操作來通知的,但是notFull條件屬于putLock,而拿走元素時,是用了takeLock,因此這里put方法在擁有putLock的情況通知notFull條件,會讓其他添加元素的方法避免過長時間的等待。同理對于take方法來說也通知notEmpty條件。
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
最后說說remove和contains方法,因為需要操作整個鏈表,因此需要同時擁有兩個鎖才能操作。
新聞熱點
疑難解答