隊列:LinkedBlockingQueue源碼解析


隊列是很重要的API,線程池、讀寫鎖、消息隊列等技術和框架的底層原理都是隊列,是很多高級API的基礎。

LinkedBlockingQueue可以理解爲一個典型的生產者-消費者模型。

1.整體架構

LinkedBlockingQueue,鏈表阻塞隊列,底層的數據結構時鏈表,且隊列是可阻塞的。
image

Queue接口和BlockingQueue接口

從類關係圖中可以看出兩條線路,

線路1
AbstractQueue --> AbstractCollection --> Collection --> Iterable

這條路線主要是想複用Collection和迭代器的一些操作。

線路2
BlockingQueue --> Queue -->Collection

Queue是最基礎的接口,幾乎所有隊列實現類都實現這個接口,該接口定義了隊列的三類基本操作,

  • 新增操作
    add方法隊列滿時拋出異常
    offer方法隊列滿時返回false
  • 查看並刪除操作
    remove方法隊列爲空時返回false
    poll方法隊列爲空時返回null
  • 只查看但不刪除操作
    element方法隊列爲空時拋出異常
    peek方法隊列爲空時返回null

BlockingQueue在Queue的基礎上添加了阻塞的概念,如一直阻塞或阻塞指定時間,

操作類型 拋異常 特殊值 阻塞 阻塞一段時間
新增直至隊列已滿 add offer返回false put offer設置超時,超時返回false
彈出隊列頭至隊列爲空 remove返回false;poll返回null take poll設置超時,超時返回null
查看隊列頭但不彈出 element peek返回null

類註釋

  1. 基於鏈表的阻塞隊列,底層數據結構是鏈表
  2. 鏈表維護先入先出隊列,新元素添加到隊尾,獲取元素時從頭部取出
  3. 鏈表的大小在初始化時候可以設置,默認是Integer的最大值
  4. 可以使用Collection和Iterable的所有操作

內部結構

LinkedBlockingQueue可以分爲三個部分,存儲鏈表+鎖+迭代器

// 鏈表節點
static class Node<E> {
    E item;

    Node<E> next;

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

// 鏈表容量,默認情況下是 Integer.MAX_VALUE
private final int capacity;

// 使用原子性的Integer類對象記錄鏈表元素數目
private final AtomicInteger count = new AtomicInteger();

// 鏈表頭
transient Node<E> head;

// 鏈表尾
private transient Node<E> last;

// 獲取元素時候的鎖,take和poll方法會用到
private final ReentrantLock takeLock = new ReentrantLock();

// 獲取元素時的條件隊列,可以理解爲 等待鏈表not empty時才獲取元素
private final Condition notEmpty = takeLock.newCondition();

// 添加元素時候的鎖,put和offer方法會用到
private final ReentrantLock putLock = new ReentrantLock();

// 添加元素時的條件隊列,可以理解爲 等待鏈表not Full時才添加元素
private final Condition notFull = putLock.newCondition();

// 內部實現的迭代器
private class Itr implements Iterator<E> {
	......
}

三種結構各司其職,

  1. 鏈表作用是保存當前節點,節點使用了泛型,所以節點中的數據可以是任意的對象。
  2. 鎖有take和put鎖,目的是保證隊列操作時線程安全,同時take和put操作可以同時進行,互不影響。

初始化

LinkedBlockingQueue有三種初始化方式,

// 無參數初始化,默認容量是Integer.MAX_VALUE
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);	// 此處調用的是指定容量初始化的構造方法(見下)
}

// 指定容量初始化
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);	// 注意,head節點一定是固定的值爲null的節點對象,保證了不會空指針異常
}

// 使用集合對象初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // Never contended, but necessary for visibility
    try {
        int n = 0;
        for (E e : c) {
            if (e == null)
                throw new NullPointerException();	// 集合類元素不能爲null
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

初始化源碼中包含的信息,

  1. 初始化時容量大小不影響性能,隻影響以後的使用,初始化隊列太小會導致過早報出IllegalStateException異常
  2. 源碼中for循環的形式並不優雅,添加一個元素後檢查是否超過capacity是一種低效的方式。完全可以先得到集合對象的size,直接判斷是否與設定的capacity衝突。

2.源碼解析

入隊和出隊操作

隊列是先進先出的結構,入隊元素會被添加到隊尾,出隊元素是隊列頭部元素。分別對應於enqueuedequeue方法。

1) enqueue方法

入隊方法很簡單,在隊尾添加節點後將last指針指向新加入的節點對象。

private void enqueue(Node<E> node) {
    last = last.next = node;
}

初始化方法中,last指針首先會知道一個item爲null的節點對象上,因此在添加節點的過程中不會出現空指針異常。

2) dequeue方法

出隊每次需要將隊列頭部元素取出,注意需要保證head指針始終指向的是item爲null的節點。

private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;	// 此時h是初始化時創建的item爲null的節點,first指向的是實際的隊列頭部
    h.next = h; // help GC
    head = first;	// head指針指向隊列頭部節點
    E x = first.item;
    first.item = null;	// 返回頭部節點的item值,並將頭部節點的item設置爲null,成爲新的head節點
    return x;
}

新增節點操作

1) add方法

add方法在容量達到capacity時會拋出異常,

public boolean add(E e) {
    if (offer(e))
        return true;
    else
        throw new IllegalStateException("Queue full");
}

可見該方法底層調用的是offer方法,通過offer方法的返回值判斷是否添加成功,如果添加失敗則會拋出異常。

2) offer方法

offer方法不會拋出異常,而是在添加成功後返回true,反之,返回false。

public boolean offer(E e) {
    if (e == null) throw new NullPointerException();
    final AtomicInteger count = this.count;
    if (count.get() == capacity)	// 隊列滿了返回false
        return false;
        
    int c = -1;		// 注意這裏 c初始化值爲是-1
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;	// 爭取到put鎖後,上鎖
    putLock.lock();
    try {
        if (count.get() < capacity) {
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();	// 如果隊列未滿,添加元素後count值值賦予c變量,之後增加
        }
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();	// 如果c從-1到0說明是第一次加入元素,隊列從空變爲非空,喚醒put等待隊列中的線程
    return c >= 0;
}

private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

3) 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>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    putLock.lockInterruptibly();
    try {
        // 如果隊列已滿則阻塞,可簡單記憶爲 wait not full,等待未滿的過程
        while (count.get() == capacity) {
            notFull.await();
        }
        enqueue(node);
        c = count.getAndIncrement();
        if (c + 1 < capacity)
            notFull.signal();	// 添加後如果還未滿,可嘗試喚醒另一個put等待隊列中的對象
    } finally {
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}

三種添加元素的方法進行如下總結,

  1. 添加元素的第一步是上鎖,保證線程安全
  2. 新增數據直接添加到隊尾即可
  3. 新增數據時,如果容量滿了,則當前線程阻塞,直至隊頭元素被取出使得隊列中存在空位。阻塞是通過鎖實現的,具體原理也是等待隊列,會在以後鎖相關的筆記中說明
  4. 添加元素成功後,如果隊列未滿,會嘗試喚醒putLock的等待線程;同時,如果隊列不爲空,會喚醒takeLock的等待線程。保證一旦滿足put或take的條件,就能夠喚起等待線程,不會浪費時間。

offer方法可以設置阻塞一定時間,具體原理與put方法相同,只是在一定時間範圍內阻塞,
image

刪除節點操作

隊列的刪除節點操作返回的是隊列頭節點的值,但是具體返回形式有兩種,一種是返回值的同時刪除頭節點,另一種是返回值但是不刪除節點。

刪除數據關注兩點,

  1. 刪除原理
  2. 查看並刪除和查看不刪除在實現方式上的區別

查看並刪除

1) take方法(阻塞)

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) {	// 如果隊列爲空,則阻塞,wait not empty
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}
  1. 先上鎖
  2. 如果隊列爲空,則阻塞,直至隊列非空;反之,使用dequeue方法刪除隊頭節點,並返回隊頭節點的值
  3. 如果滿足put或take的條件,會嘗試喚醒等待隊列中的線程

2) poll方法(非阻塞)

public E poll() {
    final AtomicInteger count = this.count;
    if (count.get() == 0)	// 如果隊列爲空直接返回null
        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;
}

與take方法相比,不存在阻塞過程。

查看但不刪除—peek方法

public E peek() {
    if (count.get() == 0)	// 隊列爲空則返回null
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        Node<E> first = head.next;
        if (first == null)
            return null;
        else
            return first.item;	// 獲取隊頭節點的item值並返回
    } finally {
        takeLock.unlock();
    }
}

該過程不涉及使用dequeue刪除對頭節點。

總結

LinkedBlockingQueue可以應用到多線程環境中,如消費者-生產者模型。隊列本身也是很重要的數據結構。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章