迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

Jay_huaxiao發表於2019-11-03

前言

整理了阻塞佇列LinkedBlockingQueue的學習筆記,希望對大家有幫助。有哪裡不正確,歡迎指出,感謝。

LinkedBlockingQueue的概述

LinkedBlockingQueue的繼承體系圖

我們先來看看LinkedBlockingQueue的繼承體系。使用IntelliJ IDEA檢視類的繼承關係圖形

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

  • 藍色實線箭頭是指類繼承關係
  • 綠色箭頭實線箭頭是指介面繼承關係
  • 綠色虛線箭頭是指介面實現關係。

LinkedBlockingQueue實現了序列化介面 Serializable,因此它有序列化的特性。 LinkedBlockingQueue實現了BlockingQueue介面,BlockingQueue繼承了Queue介面,因此它擁有了佇列Queue相關方法的操作。

LinkedBlockingQueue的類圖

類圖來自Java併發程式設計之美

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

LinkedBlockingQueue主要特性:

  1. LinkedBlockingQueue底層資料結構為單向連結串列。
  2. LinkedBlockingQueue 有兩個Node節點,一個head節點,一個tail節點,只能從head取元素,從tail新增元素。
  3. LinkedBlockingQueue 容量是一個原子變數count,它的初始值為0。
  4. LinkedBlockingQueue有兩把ReentrantLock的鎖,一把控制元素入隊,一把控制出隊,保證在併發情況下的執行緒安全。
  5. LinkedBlockingQueue 有兩個條件變數,notEmpty 和 notFull。它們內部均有一個條件佇列,存放著出入佇列被阻塞的執行緒,這其實是生產者-消費者模型。

LinkedBlockingQueue的重要成員變數

//容量範圍,預設值為 Integer.MAX_VALUE
private final int capacity;

//當前佇列元素個數
private final AtomicInteger count = new AtomicInteger();

//頭結點
transient Node<E> head;

//尾節點
private transient Node<E> last;

//take, poll等方法的可重入鎖
private final ReentrantLock takeLock = new ReentrantLock();

//當佇列為空時,執行出隊操作(比如take )的執行緒會被放入這個條件佇列進行等待
private final Condition notEmpty = takeLock.newCondition();

//put, offer等方法的可重入鎖
private final ReentrantLock putLock = new ReentrantLock();

//當佇列滿時, 執行進隊操作( 比如put)的執行緒會被放入這個條件佇列進行等待
private final Condition notFull = putLock.newCondition();
複製程式碼

LinkedBlockingQueue的建構函式

LinkedBlockingQueue有三個建構函式:

  1. 無參建構函式,容量為Integer.MAX
public LinkedBlockingQueue() {
   this(Integer.MAX_VALUE);
}
複製程式碼
  1. 設定指定容量的構造器
public LinkedBlockingQueue(int capacity) {
  if (capacity <= 0) throw new IllegalArgumentException();
   //設定佇列大小
   this.capacity = capacity;
   //new一個null節點,head、tail節點指向該節點
   last = head = new Node<E>(null);
}
複製程式碼
  1. 傳入集合,如果呼叫該構造器,容量預設也是Integer.MAX_VALUE
 public LinkedBlockingQueue(Collection<? extends E> c) {
        //呼叫指定容量的構造器
        this(Integer.MAX_VALUE);
        //獲取put, offer的可重入鎖
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); 
        try {
            int n = 0;
            //迴圈向佇列中新增集合中的元素
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                //將佇列的last節點指向該節點
                enqueue(new Node<E>(e));
                ++n;
            }
            //更新容量值
            count.set(n);
        } finally {
            //釋放鎖
            putLock.unlock();
        }
    }
複製程式碼

LinkedBlockingQueue底層Node類

Node原始碼

static class Node<E> {
    // 當前節點的元素值
    E item;
    // 下一個節點的索引
    Node<E> next;
    //節點構造器
    Node(E x) { 
     item = x;
   }
 }
複製程式碼

LinkedBlockingQueue的節點符合單向連結串列的資料結構要求:

  • 一個成員變數為當前節點的元素值
  • 一個成員變數是下一節點的索引
  • 構造方法的唯一引數節點元素值。

Node節點圖

item表示當前節點的元素值,next表示指向下一節點的指標

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

LinkedBlockingQueue常用操作

offer操作

入隊方法,其實就是向佇列的尾部插入一個元素。如果元素為空,丟擲空指標異常。如果佇列已滿,則丟棄當前元素,返回false,它是非阻塞的。如果佇列空閒則插入成功返回true。

offer原始碼

offer方法原始碼如下:

   public boolean offer(E e) {
        //為空直接拋空指標
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        //如果當前佇列滿了的話,直接返回false
        if (count.get() == capacity)
            return false;
        int c = -1;
        //構造新節點
        Node<E> node = new Node<E>(e);
        獲取put獨佔鎖
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            //判斷佇列是否已滿
            if (count.get() < capacity) {
                //進佇列
                enqueue(node);
                //遞增元素計數
                c = count.getAndIncrement();
                //如果元素入隊,還有空閒,則喚醒notFull條件佇列裡被阻塞的執行緒
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            //釋放鎖
            putLock.unlock();
        }
        //如果容量為0,則
        if (c == 0)
            //啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
            signalNotEmpty();
        return c >= 0;
    }
複製程式碼

enqueue方法原始碼如下:

private void enqueue(Node<E> node) {
 //從尾節點加進去
 last = last.next = node;
 }
複製程式碼

為了形象生動,我們用一張圖來看看往佇列裡依次放入元素A和元素B。圖片參考來源【細談Java併發】談談LinkedBlockingQueue

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

signalNotEmpty方法原始碼如下

private void signalNotEmpty() {
    //獲取take獨佔鎖
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
       //喚醒notEmpty條件佇列裡被阻塞的執行緒
       notEmpty.signal();
     } finally {
       //釋放鎖
       takeLock.unlock();
        }
    }
複製程式碼

offer執行流程圖

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

基本流程:

  • 判斷元素是否為空,如果是,就丟擲空指標異常。
  • 判讀佇列是否已滿,如果是,新增失敗,返回false。
  • 如果佇列沒滿,構造Node節點,上鎖。
  • 判斷佇列是否已滿,如果佇列沒滿,Node節點在隊尾加入佇列待。
  • 加入佇列後,判斷佇列是否還有空閒,如果是,喚醒notFull的阻塞執行緒。
  • 釋放完鎖後,判斷容量是否為空,如果是,喚醒notEmpty的阻塞執行緒。

put操作

put方法也是向佇列尾部插入一個元素。如果元素為null,丟擲空指標異常。如果佇列己滿則阻塞當前執行緒,直到佇列有空閒插入成功為止。如果佇列空閒則插入成功,直接返回。如果在阻塞時被其他執行緒設定了中斷標誌, 則被阻塞執行緒會丟擲 InterruptedException 異常而返回。

put原始碼

  public void put(E e) throws InterruptedException {
        ////為空直接拋空指標異常
        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;
        //獲取獨佔鎖,它跟lock的區別,是可以被中斷
        putLock.lockInterruptibly();
        try {
            //佇列已滿執行緒掛起等待
            while (count.get() == capacity) {
                notFull.await();
            }
            //進佇列
            enqueue(node);
            //遞增元素計數
            c = count.getAndIncrement();
            //如果元素入隊,還有空閒,則喚醒notFull條件佇列裡被阻塞的執行緒
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            //釋放鎖
            putLock.unlock();
        }
        //如果容量為0,則
        if (c == 0)
            //啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
            signalNotEmpty();
    }
複製程式碼

put流程圖

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

基本流程:

  • 判斷元素是否為空,如果是就丟擲空指標異常。
  • 構造Node節點,上鎖(可中斷鎖)
  • 判斷佇列是否已滿,如果是,阻塞當前執行緒,一直等待。
  • 如果佇列沒滿,Node節點在隊尾加入佇列。
  • 加入佇列後,判斷佇列是否還有空閒,如果是,喚醒notFull的阻塞執行緒。
  • 釋放完鎖後,判斷容量是否為空,如果是,喚醒notEmpty的阻塞執行緒。

poll操作

從佇列頭部獲取並移除一個元素, 如果佇列為空則返回 null, 該方法是不阻塞的。

poll原始碼

poll方法原始碼

 public E poll() {
        final AtomicInteger count = this.count;
        //如果佇列為空,返回null
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        //獲取takeLock獨佔鎖
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            //如果佇列不為空,則出隊,並遞減計數
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                ////容量大於1,則啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
           //釋放鎖
            takeLock.unlock();
        }
        if (c == capacity)
            //喚醒notFull條件佇列裡被阻塞的執行緒
            signalNotFull();
        return x;
    }
複製程式碼

dequeue方法原始碼

  //出佇列
  private E dequeue() {
        //獲取head節點
        Node<E> h = head;
        //獲取到head節點指向的下一個節點
        Node<E> first = h.next;
        //head節點原來指向的節點的next指向自己,等待下次gc回收
        h.next = h; // help GC
        // head節點指向新的節點
        head = first;
        // 獲取到新的head節點的item值
        E x = first.item;
        // 新head節點的item值設定為null
        first.item = null;
        return x;
    }
複製程式碼

為了形象生動,我們用一張圖來描述出隊過程。圖片參考來源【細談Java併發】談談LinkedBlockingQueue

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

signalNotFull方法原始碼

 private void signalNotFull() {
        //獲取put獨佔鎖
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            ////喚醒notFull條件佇列裡被阻塞的執行緒
            notFull.signal();
        } finally {
            //釋放鎖
            putLock.unlock();
        }
    }
複製程式碼

poll流程圖

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

基本流程:

  • 判斷元素是否為空,如果是,就返回null。
  • 加鎖
  • 判斷佇列是否有元素,如果沒有,釋放鎖
  • 如果佇列有元素,則出佇列,獲取資料,容量計數器減一。
  • 判斷此時容量是否大於1,如果是,喚醒notEmpty的阻塞執行緒。
  • 釋放完鎖後,判斷容量是否滿,如果是,喚醒notFull的阻塞執行緒。

peek操作

獲取佇列頭部元素但是不從佇列裡面移除它,如果佇列為空則返回 null。 該方法是不 阻塞的。

peek原始碼

  public E peek() {
        //佇列容量為0,返回null
        if (count.get() == 0)
            return null;
        //獲取takeLock獨佔鎖
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            //判斷first是否為null,如果是直接返回
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            //釋放鎖
            takeLock.unlock();
        }
    }
複製程式碼

peek流程圖

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

基本流程:

  • 判斷佇列容量大小是否為0,如果是,就返回null。
  • 加鎖
  • 獲取佇列頭部節點first
  • 判斷節點first是否為null,是的話,返回null。
  • 如果fist不為null,返回節點first的元素。
  • 釋放鎖。

take操作

獲取當前佇列頭部元素並從佇列裡面移除它。 如果佇列為空則阻塞當前執行緒直到佇列 不為空然後返回元素,如果在阻塞時被其他執行緒設定了中斷標誌, 則被阻塞執行緒會丟擲 InterruptedException 異常而返回。

take原始碼

 public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        //獲取takeLock獨佔鎖
        final ReentrantLock takeLock = this.takeLock;
        //獲取獨佔鎖,它跟lock的區別,是可以被中斷
        takeLock.lockInterruptibly();
        try {
            //當前佇列為空,則阻塞掛起
            while (count.get() == 0) {
                notEmpty.await();
            }
            //)出隊並遞減計數
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
               //啟用 notEmpty 的條件佇列,喚醒被阻塞的執行緒
                notEmpty.signal();
        } finally {
            //釋放鎖
            takeLock.unlock();
        }
        if (c == capacity)
            //啟用 notFull 的條件佇列,喚醒被阻塞的執行緒
            signalNotFull();
        return x;
    }
複製程式碼

take流程圖

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

基本流程:

  • 加鎖
  • 判斷佇列容量大小是否為0,如果是,阻塞當前執行緒,直到佇列不為空。
  • 如果佇列容量大小大於0,節點出佇列,獲取元素x,計數器減一。
  • 判斷佇列容量大小是否大於1,如果是,喚醒notEmpty的阻塞執行緒。
  • 釋放鎖。
  • 判斷佇列容量是否已滿,如果是,喚醒notFull的阻塞執行緒。
  • 返回出隊元素x

remove操作

刪除佇列裡面指定的元素,有則刪除並返回 true,沒有則返回 false。

remove方法原始碼

 public boolean remove(Object o) {
         //為空直接返回false
        if (o == null) return false;
        //雙重加鎖
        fullyLock();
        try {
            //邊歷佇列,找到元素則刪除並返回true
            for (Node<E> trail = head, p = trail.next;
                 p != null;
                 trail = p, p = p.next) {
                if (o.equals(p.item)) {
                    //執行unlink操作
                    unlink(p, trail);
                    return true;
                }
            }
            return false;
        } finally {
            //解鎖
            fullyUnlock();
        }
    }
複製程式碼

雙重加鎖,fullyLock方法原始碼

void fullyLock() {
        //putLock獨佔鎖加鎖
        putLock.lock();
        //takeLock獨佔鎖加鎖
        takeLock.lock();
    }
複製程式碼

unlink方法原始碼

  void unlink(Node<E> p, Node<E> trail) {
        p.item = null;
        trail.next = p.next;
        if (last == p)
            last = trail;
        //如果當前佇列滿 ,則刪除後,也不忘記喚醒等待的執行緒 
        if (count.getAndDecrement() == capacity)
            notFull.signal();
    }
複製程式碼

fullyUnlock方法原始碼

  void fullyUnlock() {
        //與雙重加鎖順序相反,先解takeLock獨佔鎖
        takeLock.unlock();
        putLock.unlock();
    }
複製程式碼

remove流程圖

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

基本流程

  • 判斷要刪除的元素是否為空,是就返回false。
  • 如果要刪除的元素不為空,加雙重鎖
  • 遍歷佇列,找到要刪除的元素,如果找不到,返回false。
  • 如果找到,刪除該節點,返回true。
  • 釋放鎖

size操作

獲取當前佇列元素個數。

 public int size() {
        return count.get();
    }
複製程式碼

由於進行出隊、入隊操作時的 count是加了鎖的,所以結果相比ConcurrentLinkedQueue 的 size 方法比較準確。

總結

  • LinkedBlockingQueue底層通過單向連結串列實現。
  • 它有頭尾兩個節點,入隊操作是從尾節點新增元素,出隊操作是對頭節點進行操作。
  • 它的容量是原子變數count,保證szie獲取的準確性。
  • 它有兩把獨佔鎖,保證了佇列操作原子性。
  • 它的兩把鎖都配備了一個條件佇列,用來存放阻塞執行緒,結合入隊、出隊操作實現了一個生產消費模型。

Java併發程式設計之美中,有一張圖惟妙惟肖描述了它,如下圖:

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

參看與感謝

個人公眾號

迴歸Java基礎:LinkedBlockingQueue阻塞佇列解析

  • 如果你是個愛學習的好孩子,可以關注我公眾號,一起學習討論。
  • 如果你覺得本文有哪些不正確的地方,可以評論,也可以關注我公眾號,私聊我,大家一起學習進步哈。

相關文章