Java隊列之LinkedBlockingQueue源碼解析

目錄

1.LinkedBlockingQueue

1.1整體架構

1.2初始化源碼解析

1.3阻塞新增源碼解析

1.4阻塞刪除源碼分析

1.5查看元素源碼分析


1.LinkedBlockingQueue


1.1整體架構

  • 主要實現了BlockingQueue和Queue接口
  • Queue接口包含了:
遇到隊列滿或空的時候,拋異常,如 add、remove、element;
遇到隊列滿或空的時候,返回特殊值,如 offer、poll、peek。
  • BlockingQueue接口包含了:
遇到隊列滿或空的時候,拋異常,如 add、remove、element;
遇到隊列滿或空的時候,返回特殊值/阻塞一段時間如 offer、poll、peek(不會進行阻塞,直接返回);
遇到隊列滿或空的時候,一直阻塞,如 put、take;
  • 底層數據結構使用鏈表,鏈表大小可以在初始化進行設置,默認是int的最大值
// 鏈表結構 begin
//鏈表的元素
static class Node<E> {
    E item;
    //當前元素的下一個,爲空表示當前節點是最後一個
    Node<E> next;
    Node(E x) { item = x; }
}
//鏈表的容量,默認 Integer.MAX_VALUE
private final int capacity;
//鏈表已有元素大小,使用 AtomicInteger,所以是線程安全的
private final AtomicInteger count = new AtomicInteger();
//鏈表頭
transient Node<E> head;
//鏈表尾
private transient Node<E> last;
// 鏈表結構 end

// 鎖 begin
//take 時的鎖
private final ReentrantLock takeLock = new ReentrantLock();
// take 的條件隊列,condition 可以簡單理解爲基於 ASQ 同步機制建立的條件隊列
private final Condition notEmpty = takeLock.newCondition();
// put 時的鎖,設計兩把鎖的目的,主要爲了 take 和 put 可以同時進行
private final ReentrantLock putLock = new ReentrantLock();
// put 的條件隊列
private final Condition notFull = putLock.newCondition();
// 鎖 end

// 迭代器 
// 實現了自己的迭代器
private class Itr implements Iterator<E> {
………………
}

1.2初始化源碼解析

// 不指定容量,默認 Integer 的最大值
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE); //調用指定容量初始化
}

// 指定鏈表容量大小,鏈表頭尾相等,節點值(item)都是 null
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) 
    this.capacity = capacity;
    // 頭節點和尾節點永遠不爲空,會有一個head節點一直會指向這個哨兵節點!!!
    last = head = new Node<E>(null);
}

// 已有集合數據進行初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);//調用指定容量初始化
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // 獲取鎖
    try {
        int n = 0;
        for (E e : c) {
            // 集合內的元素不能爲空
            if (e == null)
                throw new NullPointerException();
            // capacity 代表鏈表的大小,在這裏是 Integer 的最大值
            // 如果集合類的大小大於 Integer 的最大值,就會報錯
            // 其實這個判斷完全可以放在 for 循環外面,這樣可以減少 Integer 的最大值次循環(最壞情況)
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

第一種:不指定初始容量默認爲int的最大值

第二種:指定初始值大小,

第三種:指定集合來進行初始化,需要加put鎖來保證線程安全性

ps:三種初始化其實都是指定大小的初始化,只不過進行了封裝,頭節點永遠的會指向一個空值的哨兵節點,可以簡化編程的複雜程度!!!

1.3阻塞新增源碼解析

// 把e新增到隊列的尾部。
public void put(E e) throws InterruptedException {
    // e 爲空,拋出異常
    if (e == null) throw new NullPointerException();
    // 預先設置 c 爲 -1,約定負數爲新增失敗
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock; //獲取鎖
    final AtomicInteger count = this.count; //獲取節點值
    putLock.lockInterruptibly(); // 可中斷鎖
    try {
        // 隊列滿了
        // 當前線程阻塞,等待其他線程的喚醒(其他線程 take 成功後一定會喚醒此處被阻塞的線程)
        while (count.get() == capacity) {
            // await 無限等待
            notFull.await();
        }
        // 隊列沒有滿,直接新增到隊列的尾部
        enqueue(node);
        // 新增計數賦值,注意這裏 getAndIncrement 返回的是舊值
        // 這裏的 c 是比真實的 count 小 1 的
        c = count.getAndIncrement();
        // 如果鏈表現在的大小 小於鏈表的容量,說明隊列未滿
        // 可以嘗試喚醒一個 put 的等待線程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // 釋放鎖
        putLock.unlock();
    }
    // c==0,代表隊列裏面有一個元素,會嘗試喚醒一個take的等待線程
    if (c == 0)
        signalNotEmpty();
}
// 入隊,把新元素放到隊尾
private void enqueue(Node<E> node) {
    last = last.next = node; //很簡潔,第二個等號賦值後會返回node的值
}

獲取到put鎖,獲取到原子類型count(底層使用的CAS操作),並且設置鎖可中斷,如果隊列已滿就會進行無期限等待(需要手動的進行喚醒)在進行尾部添加節點並且累加count值,如果鏈表沒滿會嘗試喚醒一個put線程,然後釋放鎖。

最後在判斷增加後隊列是否只有一個值,如果是的話需要進行喚醒一個take線程

ps:尾部添加元素由於有了哨兵節點(head節點)變得非常優雅簡介!否則是需要判斷空鏈表,進行更新head節點的。

1.4阻塞刪除源碼分析

// 阻塞拿數據
public E take() throws InterruptedException {
    E x;
    // 默認負數,代表失敗
    int c = -1;
    // count 代表當前鏈表數據的真實大小
    final AtomicInteger count = this.count;// 獲取原子類型進行操作
    final ReentrantLock takeLock = this.takeLock;// 獲取take鎖
    takeLock.lockInterruptibly();// 可中斷鎖
    try {
        // 空隊列時,會被阻塞,等待其他線程喚醒
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 非空隊列,從隊列的頭部拿一個出來
        x = dequeue();
        // 原子操作減一,返回的值是舊值
        c = count.getAndDecrement();
        // 如果隊列裏面有值,從 take 的等待線程裏面喚醒一個。
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 釋放鎖
        takeLock.unlock();
    }
    // 已經刪除了一個元素,
    // 如果隊列空閒還剩下一個,嘗試從 put 的等待線程中喚醒一個
    if (c == capacity)
        signalNotFull();
    return x;
}
// 隊頭中取數據
// 每次更新的實際是哨兵節點,是哨兵節點不停地在移動
// 把原本的哨兵節點變爲循環引用,然後更新哨兵爲下一個元素,並將此節點變爲哨兵節點
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;
}

整個過程和put方法大致類似,取頭節點較爲複雜(每次實際爲哨兵節點在不停的移動)

獲取take鎖,獲取原子類型隊列的count,進行加鎖並且設置鎖爲可中斷,如果隊列爲空會無限阻塞(直到有人喚醒),然後從頭節點獲取值並且刪除該節點,如果隊列還有值會嘗試喚醒一個take線程,然後釋放鎖,

如果隊列在取元素之前是滿的那麼會嘗試喚醒一個put線程,最後返回節點的值。

1.5查看元素源碼分析

// 查看並不刪除元素,如果隊列爲空,返回 null
public E peek() {
    // count 代表隊列實際大小,隊列爲空,直接返回 null
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        // 拿到隊列頭
        Node<E> first = head.next;
        // 判斷隊列頭是否爲空,並返回
        // 不會被進行阻塞
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

只是取元素但不進行刪除,還是會獲取take鎖防止節點被改變,主要就是進行了判斷隊列的空條件,不會被阻塞!

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