(一)、概述
上一篇文章中我們介紹了ArrayBlockingQueue的具體實現,這一篇我們將介紹一個差不多的阻塞隊列,不過是使用鏈表實現的LinkedBlockingQueue。LinkedBlockingQueue是使用兩把ReentrantLock實現的,分別是takeLock(用於保證取出數據安全)、putLock(用於保證插入數據安全)。我們通過上一篇瞭解到ArrayBlockingQueue是用一把鎖實現的,而LinkedBlockingQueue是用兩把鎖實現的。所以相比之下Linked會擁有更好的併發量。
(二)、成員變量
//阻塞隊列的容量
private final int capacity;
//使用AtomicInteger來保證原子性,count用來記錄實際有多少個數據
private final AtomicInteger count = new AtomicInteger();
//節點類
transient Node<E> head;
//尾節點
private transient Node<E> last;
//獲取數據的鎖
private final ReentrantLock takeLock = new ReentrantLock();
//獲取數據的等待隊列notEmpty,如果阻塞隊列爲空時,獲取數據的線程會進入
private final Condition notEmpty = takeLock.newCondition();
//插入數據的鎖
private final ReentrantLock putLock = new ReentrantLock();
//插入數據的等待隊列notFull,如果阻塞隊列已經滿了的時候,插入數據的線程會進入
private final Condition notFull = putLock.newCondition();
上面需要注意的就是LInkedBlockingQueue會持有兩把鎖,分別是takeLock和putLock,保證了更好的併發量。
(三)、put方法
下面我們來看一下生產者線程中的一個重要方法:put()方法。主要用於生產者線程進行插入數據。
public void put(E e) throws InterruptedException {
//判斷要插入的元素是否爲null
if (e == null) throw new NullPointerException();
int c = -1;
//新建一個節點
Node<E> node = new Node<E>(e);
//獲得putLock
final ReentrantLock putLock = this.putLock;
//獲得插入前數據的總數
final AtomicInteger count = this.count;
//嘗試獲得putLock(可響應中斷)
putLock.lockInterruptibly();
try {
//如果阻塞隊列已經達到最大容量
while (count.get() == capacity) {
//進行等待操作
notFull.await();
}
//入隊操作
enqueue(node);
//總數+1,返回值c是插入之前的總數
c = count.getAndIncrement();
//如果插入了這個數據,隊列還沒滿,就繼續從notFull隊列中喚醒線程進行插入數據
if (c + 1 < capacity)
notFull.signal();
} finally {
//釋放鎖
putLock.unlock();
}
//如果插入之前個數爲0,就喚醒消費者的線程
if (c == 0)
signalNotEmpty();
}
上面是put()方法的源碼,具體實現應該不難,這裏就不進行過多解釋,看上面的註釋應該很清晰。
可以看到put()方法調用了enqueue()方法進行入隊操作。
private void enqueue(Node<E> node) {
//添加一個節點
last = last.next = node;
}
enqueue方法也並不是很難理解,只是簡單的進行入隊操作,所以也不過多解釋了。
(四)、take方法
下面是消費者獲取數據的重要方法:take()方法。主要用在需要獲取數據的線程進行數據的獲取
public E take() throws InterruptedException {
E x;
int c = -1;
//阻塞隊列中數據的實際個數
final AtomicInteger count = this.count;
//獲得takeLock
final ReentrantLock takeLock = this.takeLock;
//嘗試獲取鎖(可響應中斷)
takeLock.lockInterruptibly();
try {
//如果數據的個數爲0
while (count.get() == 0) {
//進行等待
notEmpty.await();
}
//出隊操作
x = dequeue();
//總數-1,返回值是出隊之前的總數
c = count.getAndDecrement();
//如果還有數據,就繼續喚醒消費者線程,取出數據
if (c > 1)
notEmpty.signal();
} finally {
//釋放鎖
takeLock.unlock();
}
//如果取數據之前,隊列是滿的,那麼現在有位置了,喚醒生產者的線程
if (c == capacity)
signalNotFull();
return x;
}
take方法的邏輯也不難,這裏也不解釋太多,我們直接來看dequeue()方法:
private E dequeue() {
//進行鏈表的一些基本操作,出隊操作
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}