1. 程式人生 > >Java併發程式設計之LinkedBlockingDeque阻塞佇列詳解

Java併發程式設計之LinkedBlockingDeque阻塞佇列詳解

簡介

LinkedBlockingDeque是一個由連結串列結構組成的雙向阻塞佇列,即可以從佇列的兩端插入和移除元素。雙向佇列因為多了一個操作佇列的入口,在多執行緒同時入隊時,也就減少了一半的競爭。

相比於其他阻塞佇列,LinkedBlockingDeque多了addFirst、addLast、peekFirst、peekLast等方法,以first結尾的方法,表示插入、獲取獲移除雙端佇列的第一個元素。以last結尾的方法,表示插入、獲取獲移除雙端佇列的最後一個元素。

LinkedBlockingDeque是可選容量的,在初始化時可以設定容量防止其過度膨脹,如果不設定,預設容量大小為Integer.MAX_VALUE。

LinkedBlockingDeque類有三個構造方法:

public LinkedBlockingDeque()
public LinkedBlockingDeque(int capacity)
public LinkedBlockingDeque(Collection<? extends E> c)

LinkedBlockingDeque原始碼詳解

LinkedBlockingDeque類定義為:

public class LinkedBlockingDeque<E> extends AbstractQueue<E> implements BlockingDeque<E>, java.io.Serializable

該類繼承自AbstractQueue抽象類,又實現了BlockingDeque介面,下面介紹一個BlockingDeque介面,該介面定義如下:

public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E>

BlockingDeque繼承自BlockingQueue和Deque介面,BlockingDeque介面定義了在雙端佇列中常用的方法。

LinkedBlockingDeque類中的資料都被封裝成了Node物件:

static final class Node<E> {
    E item;
    Node<E> prev;
    Node<E> next;

    Node(E x) {
        item = x;
    }
}

LinkedBlockingDeque類中的重要欄位如下:

// 佇列雙向連結串列首節點
transient Node<E> first;
// 佇列雙向連結串列尾節點
transient Node<E> last;
// 雙向連結串列元素個數
private transient int count;
// 雙向連結串列最大容量
private final int capacity;
// 全域性獨佔鎖
final ReentrantLock lock = new ReentrantLock();
// 非空Condition物件
private final Condition notEmpty = lock.newCondition();
// 非滿Condition物件
private final Condition notFull = lock.newCondition();

LinkedBlockingDeque類的底層實現和LinkedBlockingQueue類很相似,都有一個全域性獨佔鎖,和兩個Condition物件,用來阻塞和喚醒執行緒。

LinkedBlockingDeque類對元素的操作方法比較多,我們下面以putFirst、putLast、pollFirst、pollLast方法來對元素的入隊、出隊操作進行分析。

入隊

putFirst(E e)方法是將指定的元素插入雙端佇列的開頭,原始碼如下:

public void putFirst(E e) throws InterruptedException {
    // 若插入元素為null,則直接丟擲NullPointerException異常
    if (e == null) throw new NullPointerException();
    // 將插入節點包裝為Node節點
    Node<E> node = new Node<E>(e);
    // 獲取全域性獨佔鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        while (!linkFirst(node))
            notFull.await();
    } finally {
        // 釋放全域性獨佔鎖
        lock.unlock();
    }
}

入隊操作是通過linkFirst(E e)方法來完成的,如下所示:

private boolean linkFirst(Node<E> node) {
    // assert lock.isHeldByCurrentThread();
    // 元素個數超出容量。直接返回false
    if (count >= capacity)
        return false;
    // 獲取雙向連結串列的首節點
    Node<E> f = first;
    // 將node設定為首節點
    node.next = f;
    first = node;
    // 若last為null,設定尾節點為node節點
    if (last == null)
        last = node;
    else
        // 更新原首節點的前驅節點
        f.prev = node;
    ++count;
    // 喚醒阻塞在notEmpty上的執行緒
    notEmpty.signal();
    return true;
}

若入隊成功,則linkFirst(E e)方法返回true,否則,返回false。若該方法返回false,則當前執行緒會阻塞在notFull條件上。

putLast(E e)方法是將指定的元素插入到雙端佇列的末尾,原始碼如下:

public void putLast(E e) throws InterruptedException {
    // 若插入元素為null,則直接丟擲NullPointerException異常
    if (e == null) throw new NullPointerException();
    // 將插入節點包裝為Node節點
    Node<E> node = new Node<E>(e);
    // 獲取全域性獨佔鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        while (!linkLast(node))
            notFull.await();
    } finally {
        // 釋放全域性獨佔鎖
        lock.unlock();
    }
}

該方法和putFirst(E e)方法幾乎一樣,不同點在於,putLast(E e)方法通過呼叫linkLast(E e)方法來插入節點:

private boolean linkLast(Node<E> node) {
    // assert lock.isHeldByCurrentThread();
    // 元素個數超出容量。直接返回false
    if (count >= capacity)
        return false;
    // 獲取雙向連結串列的尾節點
    Node<E> l = last;
    // 將node設定為尾節點
    node.prev = l;
    last = node;
    // 若first為null,設定首節點為node節點
    if (first == null)
        first = node;
    else
        // 更新原尾節點的後繼節點
        l.next = node;
    ++count;
    // 喚醒阻塞在notEmpty上的執行緒
    notEmpty.signal();
    return true;
}
若入隊成功,則linkLast(E e)方法返回true,否則,返回false。若該方法返回false,則當前執行緒會阻塞在notFull條件上。

出隊

pollFirst()方法是獲取並移除此雙端佇列的首節點,若不存在,則返回null,原始碼如下:

public E pollFirst() {
    // 獲取全域性獨佔鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return unlinkFirst();
    } finally {
        // 釋放全域性獨佔鎖
        lock.unlock();
    }
}

移除首節點的操作是通過unlinkFirst()方法來完成的:

private E unlinkFirst() {
    // assert lock.isHeldByCurrentThread();
    // 獲取首節點
    Node<E> f = first;
    // 首節點為null,則返回null
    if (f == null)
        return null;
    // 獲取首節點的後繼節點
    Node<E> n = f.next;
    // 移除first,將首節點更新為n
    E item = f.item;
    f.item = null;
    f.next = f; // help GC
    first = n;
    // 移除首節點後,為空佇列
    if (n == null)
        last = null;
    else
        // 將新的首節點的前驅節點設定為null
        n.prev = null;
    --count;
    // 喚醒阻塞在notFull上的執行緒
    notFull.signal();
    return item;
}

pollLast()方法是獲取並移除此雙端佇列的尾節點,若不存在,則返回null,原始碼如下:

public E pollLast() {
    // 獲取全域性獨佔鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return unlinkLast();
    } finally {
        // 釋放全域性獨佔鎖
        lock.unlock();
    }
}

移除尾節點的操作是通過unlinkLast()方法來完成的:

private E unlinkLast() {
    // assert lock.isHeldByCurrentThread();
    // 獲取尾節點
    Node<E> l = last;
    // 尾節點為null,則返回null
    if (l == null)
        return null;
    // 獲取尾節點的前驅節點
    Node<E> p = l.prev;
    // 移除尾節點,將尾節點更新為p
    E item = l.item;
    l.item = null;
    l.prev = l; // help GC
    last = p;
    // 移除尾節點後,為空佇列
    if (p == null)
        first = null;
    else
        // 將新的尾節點的後繼節點設定為null
        p.next = null;
    --count;
    // 喚醒阻塞在notFull上的執行緒
    notFull.signal();
    return item;
}

其實LinkedBlockingDeque類的入隊、出隊操作都是通過linkFirst、linkLast、unlinkFirst、unlinkLast這幾個方法來實現的,原始碼讀起來也比較簡單。

相關部落格

參考資料

方騰飛:《Java併發程式設計的藝術》