背景
- 前幾天看到一道面試題:實現一個阻塞隊列,就萌生了動手操作一把的想法。看着挺簡單的,思路也和清晰,就是用ReentantLock和Condition來實現,但在實際操作過程中還是遇到了問題,總結一下,僅供參考。
阻塞隊列第一版
- 先附上第一版的代碼。內部存儲,爲了方便就使用LinkedList來實現了。
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* NOTE:
*
* @author lizhiyang
* @Date 2019-11-27 19:38
*/
public class MyBlockingQueue<E> {
private LinkedList<E> list = new LinkedList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition notFullCondition = lock.newCondition();
private Condition notEmptyCondition = lock.newCondition();
private int maxSize = 0;
public MyBlockingQueue(int maxSize) {
this.maxSize = maxSize;
}
public void push(E e) throws InterruptedException {
lock.lock();
try {
if(list.size() >= maxSize) {
notFullCondition.await();
}
list.add(e);
notEmptyCondition.signalAll();
} finally {
lock.unlock();
}
}
public E pop() throws InterruptedException {
lock.lock();
try {
if(list.size() == 0) {
notEmptyCondition.await();
}
E e = list.pop();
notFullCondition.signalAll();
return e;
} finally {
lock.unlock();
}
}
}
第一版遇到的問題
- 初始測試的時候,使用一個線程循環放數據,另一個線程循環取數據,一放一取有序進行,沒有什麼問題。
- 接着增加消費線程,兩個線程取數據,此時一個消費線程執行時拋出了異常,在取數據的時候,因爲LinkedList裏已經沒有元素了。很明顯的併發問題。但是疑問就來了,pop方法入口明明用了lock.lock()方法,已經加鎖了,怎麼還會有併發問題了
java.util.NoSuchElementException
at java.util.LinkedList.removeFirst(LinkedList.java:270)
at java.util.LinkedList.pop(LinkedList.java:801)
at com.lizhy.test.MyBlockingQueue.pop(MyBlockingQueue.java:63)
at com.lizhy.test.ThreadTest.lambda$blockingQueue$6(ThreadTest.java:94)
at java.lang.Thread.run(Thread.java:748)
- 爲了排查這個疑問,在加鎖前後和釋放鎖的地方加了一些日誌,打印lock的狀態,從日誌來看,兩個線程都加鎖成功了,並且也沒有釋放鎖的日誌
lock afterjava.util.concurrent.locks.ReentrantLock@771ae0c1[Locked by thread t1]
lock afterjava.util.concurrent.locks.ReentrantLock@771ae0c1[Locked by thread t3]
- 那麼就很奇怪了,爲什麼兩個線程都會加鎖成功呢。排查代碼,感覺可能出問題的就是在condition.await方法了。爲了確定問題所在,把這個代碼註釋,重新運行,這個時候就只有一個線程加鎖成功了。
condion.await()解惑
- 翻看了Consition.await()API,有這麼一段話
Causes the current thread to wait until it is signalled or interrupted.
The lock associated with this Condition is atomically released and the current thread becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:
- Some other thread invokes the signal method for this Condition and the current thread happens to be chosen as the thread to be awakened; or
- Some other thread invokes the signalAll method for this Condition; or
- Some other thread interrupts the current thread, and interruption of thread suspension is supported; or
- A “spurious wakeup” occurs.
In all cases, before this method can return the current thread must re-acquire the lock associated with this condition. When the thread returns it is guaranteed to hold this lock.
- 大概的意思就是說:調用這個方法,當前線程會被阻塞,直到有人喚醒或者被打斷,並且Condition關聯的鎖會被自動釋放。有四種情況能夠喚醒線程。在所有的4中情況中,線程被喚醒,方法返回之前,都必須重新獲取關聯Condition的鎖。必須確定方法返回時,當前線程已經重新獲取到了鎖。
- 看了以上的說明,也就明瞭了。當線程一調用lock.lock()獲取了鎖,調用Condition.await()方法時,釋放鎖,線程阻塞,此時,線程二也就可以正常獲取鎖了
NoSuchElement的錯誤又是怎麼來的呢
- 通過以上分析,我們知道調用Condition.await()方法會釋放鎖,但如果線程被喚醒肯定是因爲list中有數據了,可以取了,那怎麼又報這個錯誤了呢?
- 繼續分析代碼,await被喚醒肯定是因爲有數據了,並且也獲取到了鎖,那怎麼後邊取的時候又沒有了呢?喚醒的時候用的是signalAll,會喚醒所有的線程,但是隻有一個線程能獲取到鎖,其他線程會持續嘗試獲取鎖,當第一個線程獲取鎖之後,執行了代碼,最後釋放鎖,此時,被喚醒的其他線程就又能夠獲取鎖了,注意:此時隊列裏的數據已經空了,因此就出現了NoSuchElement的錯誤
- 那要怎麼解決呢?
方法一:用signal,只喚醒一個線程,因爲是阻塞隊列,每次也只能有一個線程執行。
方法二:await之後,再次判斷一下隊列是否爲空就行了,把if改成while即可
阻塞隊列第二版
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* NOTE:
*
* @author lizhiyang
* @Date 2019-11-27 19:38
*/
public class MyBlockingQueue<E> {
private LinkedList<E> list = new LinkedList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition notFullCondition = lock.newCondition();
private Condition notEmptyCondition = lock.newCondition();
private int maxSize = 0;
public MyBlockingQueue(int maxSize) {
this.maxSize = maxSize;
}
public void push(E e) throws InterruptedException {
lock.lock();
try {
while(list.size() >= maxSize) {
notFullCondition.await();
}
list.add(e);
notEmptyCondition.signal();
} finally {
lock.unlock();
}
}
public E pop() throws InterruptedException {
lock.lock();
try {
while(list.size() == 0) {
notEmptyCondition.await();
}
E e = list.pop();
notFullCondition.signal();
return e;
} finally {
lock.unlock();
}
}
}
- 本次實現阻塞隊列遇到的問題,究其原因還是因爲對await和signal、signalAll方法的含義理解不清楚導致的。以後還得繼續加深,不能想當然,一定得動手實踐。