JAVA---阻塞隊列

問題是最好的老師!

一、思考

  • 問題一:阻塞隊列阻塞了什麼操作?爲什麼需要這樣一個隊列(使用場景是什麼)?

我們都知道在線程池中會用到阻塞隊列,那麼線程池爲什麼不用一個一般的隊列,要用阻塞隊列。那麼阻塞隊列是否還有其他場景。

  • 問題二:阻塞隊列是如何實現阻塞/喚醒的?以及何時阻塞/喚醒?

我們知道常見的阻塞喚醒方式有Object.wait/notify和Condition.await/signal。

  • 問題二:多線程情況下,阻塞隊列是否是線程安全的,可以試想如果不加同步,那麼多個線程修改隊列的結構,勢必會出現問題,阻塞隊列是如何處理的?

即多線程情況下,入隊列和出隊列是否需要進行同步。

  • 問題三:有哪幾種常見的阻塞隊列實現方式,其差異性在哪裏,各有什麼優缺點,如何選擇?

比如我們在使用線程池時,選用什麼阻塞隊列比較合適。其默認的阻塞隊列是什麼方式的。

  • 問題四:線程池中阻塞隊列設置多大合適?依據是什麼?

以上問題等分析完源碼之後,統一分析,也會在源碼分析過程中穿插分析。

二、源碼解讀

  • 註釋翻譯

在不瞭解阻塞隊列設計初衷的情況下,可以參考阻塞隊列(BlockingQueue)註釋,瞭解其基本作用,其翻譯如下:

阻塞隊列是JAVA隊列中的一種(先進先出),但是其提供額外操作:

1.支持當從隊列中獲取元素時,如果隊列爲空,則進行等待直到隊列不爲空返回元素。

2.支持當向隊列中添加元素時,如果隊列已滿,則進行等待直到隊列有可用空間入隊。

對於阻塞隊列的不同操作(方法),當其不能立即成功時(比如獲取一個元素時,隊列可能爲空),會表現出四種形式,但是這些操作在未來的某一個時間點會成功(比如當隊列不爲空,獲取數據成功),這四種表現形式如下:

  • Throws an Exception(拋出一個異常)。
  • Special Value(返回一個值,獲取元素時返回元素,成功添加到隊列返回true等待,可能返回null和fasle,取決於具體操作)。
  • Blocks(阻塞當前線程,直到操作可以成功)。
  • Time Out(在放棄當前操作前,阻塞給定時間)。

入隊操作包含以下方法,後續會有源碼分析:

  • boolean   add(e):添加元素到隊尾,添加成功返回true,若當前無可用空間拋出異常。
  • boolean   offer(e):添加元素到隊尾,添加成功返回true,若當前無可用空間返回fasle。
  • void         put(e):添加元素到隊尾,如果無可用空間則等待直到空間可用。
  • boolean   offer(e,time,unit):添加元素到隊尾,若無可用空間,等待指定時間,若等待時間到,仍無可用空間,返回false,否則返回true。

出隊操作(移除元素)包含以下方法,後續會有源碼分析:

  • Boolean   remove(e):從隊列中移除指定元素,如果包含指定元素且移除成功,返回true,否則返回fasle。
  • E              poll():返回對頭元素,如果隊列爲空返回null。
  • E              take():返回對頭元素,如果隊列爲空則等待,直到隊列中有元素。
  • E              poll(time,unit):返回對頭元素,如果隊列爲空則等待指定時間,若等待時間到,隊列中仍無元素,返回null。

返回對頭元素(不移除元素):

  • peek():隊列爲空返回null,不爲空返回對頭元素。
  • element():隊列爲空拋出異常,不爲空返回對頭原色。

總的來說,對應上面翻譯的四種表現形式,對於入隊操作,要麼成功,要麼失敗,要麼阻塞,要麼拋出異常,對於出隊操作要麼返回對應元素,要麼返回null,要麼阻塞。

阻塞隊列不接受空元素(null)入隊,因爲null有特殊意義,其表示poll操作如果隊列爲空的返回標識。

阻塞隊列的實現是線程安全的。

  • 常見實現

阻塞隊列的實現(JUC包下,本文不包括Deque雙向隊列的實現部分分析),包括以下幾種,分別對應不同功能:

  • ArrayBlockingQueue:底層數據結構爲數組的先進先出阻塞隊列,有限。
  • LinkedBlockingQueue:底層數據結構爲鏈表的先進先出阻塞隊列,有限/無限(int最大值)可自主選擇。
  • PriorityBlockingQueue:優先級隊列,底層數據結構爲數組的阻塞隊列,無限。
  • LinkedTransferQueue:底層數據結構爲鏈表的先進先出阻塞隊列,生產者會等待消費者消費,無限。
  • DelayQueue:延遲隊列,每個元素過期之後,才能被取走。
  • SynchronousQueue:入隊操作必須等待與之相關的出隊操作完成,反之亦然。
  • 源碼解讀

先從最基本的阻塞隊列開始分析

  1. ArrayBlockingQuque

  • 屬性
//隊列中存在的元素個數
int count;

//隊列存儲元素的數據結構
final Object[] items;


//以下兩個屬性是出於這樣的考慮:
//對於數組來說,如果對頭元素下標始終是0,則每次移除對頭元素需要移動後續所有元素,性能不高
//使用下面兩個屬性即該數組相當於循環數組,不涉及數據的移動

//下一次出隊元素的位置:對頭元素所在位置,每次出隊之後該值加一,超過容量爲0
int takeIndex;

//下一次入隊元素的位置:隊尾元素的後一個位置,超過容量爲0
int putIndex;

//實現線程安全操作
final ReentrantLock lock;

//阻塞隊列的核心在於支持
//1.入隊時,無可用空間則阻塞:對應notFull
//2.出隊時,無可用元素則阻塞:對應notEmpty

//獲取元素時阻塞/喚醒條件
private final Condition notEmpty;

//插入元素時阻塞/喚醒條件
private final Condition notFull;
  • 構造函數
//數組形式的阻塞隊列爲有限隊列,其容量爲capacity。
public ArrayBlockingQueue(int capacity) {
        //請看下文的構造函數
        this(capacity, false);
    }
//capacity:隊列容量
//fair:是否公平鎖
public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        //直接初始化數組大小爲容量上限
        this.items = new Object[capacity];
        //默認採用非公平的可重入鎖,實現線程安全
        lock = new ReentrantLock(fair);
        //初始化Condition
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

 

//該構造函數支持根據傳入的集合,初始化隊列
public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                //循環入隊
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            //此時takeIndex肯定爲0
            //如果隊列已滿,那下一次入隊肯定是從對頭開始,因爲出隊必定在對頭,只有對頭纔有空閒位置,隊列未滿,則putIndex未i,即隊尾元素的下一個位置。
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }
  • 主要方法

入隊主要操作:

boolean add(e):入隊,隊列已滿則拋出異常

public boolean add(E e) {
        //調用父類AbstractQueue的add(e)方法,請看下文
        return super.add(e);
}

//可以看出該方法主要調用了offer(e)方法,詳情請看offer(e)
方法詳解
//offer方法隊列爲滿返回fasle,該方法則再次封裝拋出異常而已
public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
}

 boolean offer(e):入隊,隊列爲滿返回false

public boolean offer(E e) {
        //元素不能爲空,爲什麼不能爲空,前面翻譯中有解釋
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //線程安全
        lock.lock();
        try {
            //隊列已滿,直接返回false,put方法則不同,隊列已滿就會阻塞,具體請看下文對其實現
            if (count == items.length)
                return false;
            else {
                //否則入隊,詳情請看入隊操作
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
}

 void  enque(e):入隊操作

private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        //插入數據到putIndex位置
        items[putIndex] = x;
        //如果隊列已滿,更新下一個插入位置爲0,因爲這個數組是一個循環數組
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        //阻塞隊列的出隊操作如果隊列沒有元素會在notEmpty上進行阻塞,此處只有元素入隊就進行喚醒
        //這樣只有有線程在等待該條件就能被喚醒其中一個
        notEmpty.signal();
}

 boolean offer(e,time,unit):入隊,如果隊列已滿,則等待time時間,之後如果隊列還是已滿,則返回false。

public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        //這裏可響應中斷
        lock.lockInterruptibly();
        try {
            //循環的目的
            //1.使等待時間不小於nanos秒
            //2.等待時間到之後,再次判斷隊列是否已滿
            while (count == items.length) {
                if (nanos <= 0)
                    //等待時間到,隊列還是滿,返回fasle
                    return false;
                //返回的nanos爲已等待nanos-nanos秒,剩餘nanos秒
                //注意這個地方也是會響應notFull.signal()喚醒操作的
                nanos = notFull.awaitNanos(nanos);
            }
            //隊列未滿入庫,請看上文enque(e)方法
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
}

void put(e):入隊,隊列已滿則一直等待

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //循環的目的是通知後進行二次確認,因爲在await()時,鎖已經被釋放,可能其他線程被通知,已經put
            while (count == items.length)
                //隊列已滿,則等待notFull.signal()
                notFull.await();
            //隊列未滿,重新入隊,這個時候可以安全入隊,因爲await()被喚醒之後,該線程又重新獲得鎖

            enqueue(e);
        } finally {
            lock.unlock();
        }
}

出隊主要操作:

E poll():出隊操作,隊列不爲空,返回takeIndex位置元素,否則返回null

public E poll() {
        final ReentrantLock lock = this.lock;
        //線程安全
        lock.lock();
        try {
            //隊列爲空返回null,否則出隊,請看下文dequeue方法
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
}

E dequeue():出隊頭元素:

private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //返回takeIndex位置元素
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        //循環數組
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        //暫時不分析
        if (itrs != null)
            itrs.elementDequeued();
        //出隊之後,喚醒可能在notFull上阻塞的線程,表示隊列未滿,可以入隊
        notFull.signal();
        return x;
}

 E poll(time,unit):出隊,隊列爲空等待time時間,時間到,隊列爲空,則返回null

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        //可響應中斷
        lock.lockInterruptibly();
        try {
            //隊列爲空,等待
            while (count == 0) {
                //隊列爲空,無剩餘等待時間則返回null,如果依然不能理解,請看上文offer的解釋,依然不能理解,請看另一篇博文線程池中的解釋
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            //隊列不爲空,出隊,請看上午dequeue方法
            return dequeue();
        } finally {
            lock.unlock();
        }
}

E take():出隊,隊列爲空則等待直到隊列中有元素返回:

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //這裏循環的目的有點類似於put,因爲await會釋放鎖,所以需要二次判斷
            while (count == 0)
                //這裏區別於offer(time,unit)的awaitNanos(time,unit)
                notEmpty.await();
            //隊列不爲空,出隊
            return dequeue();
        } finally {
            lock.unlock();
        }
}

三、問題分析(回答開篇)

上文源碼解釋中分析了ArrayBlockingQueue阻塞隊列的各個方法的具體實現,對於LinkedBlockingQueue,數據結構不同,思想相同,對於其他類型的隊列,在此思想的基礎上,進行擴展,添加了一些格外功能。

  • 問題一:阻塞隊列阻塞了什麼操作?爲什麼需要這樣一個隊列(使用場景是什麼)?

1.阻塞隊列支持入隊,隊列如果已滿進行阻塞;出隊隊列爲空,進行阻塞的操作。

2.我們控制這樣一個隊列,可以支持高併發下數據處理(其線程安全),以及數據緩存(可爲有限隊列)。

3.線程池中用阻塞隊列不用一般隊列,是爲了更好的實現線程的複用,當隊列中無可用元素時,核心線程可一直阻塞,當有元素可用時進行處理。試想如果不用這樣的阻塞形式,一直循環判斷隊列是否爲空判斷隊列是否有元素,那就相當於程序中寫了一個死循環。

  • 問題二:阻塞隊列是如何實現阻塞/喚醒的?以及何時阻塞/喚醒?

通過上面的源碼分析,我們瞭解到,阻塞隊列的阻塞和喚醒是通過Conditon.await/signal實現。

因爲分別需要對應入隊和出隊的阻塞喚醒操作,所以維護了兩個Conditon條件,分別如下:

1.notEmpty:控制出隊,出隊時,隊列爲空,則notEmpty.await()等待,當有元素入隊時notEmpty.signal()喚醒等待出隊線程。

2.notFull:控制入隊,入隊時,對列已滿,則notFull.await()等待,當有元素出隊時notFull.signal()喚醒等待入隊線程。

  • 問題二:多線程情況下,阻塞隊列是否是線程安全的,可以試想如果不加同步,那麼多個線程修改隊列的結構,勢必會出現問題,阻塞隊列是如何處理的?

通過源碼分析,已經瞭解到阻塞隊列的入隊和出隊都是線程安全的,通過ReentrantLock加鎖。

  • 問題三:有哪幾種常見的阻塞隊列實現方式,其差異性在哪裏,各有什麼優缺點,如何選擇?

前文中已經提到常見阻塞隊列的實現,並簡單介紹了其作用。

1.ArrayBlockingQueue:因爲其是有界的,所以當需要有界隊列時,可以選擇使用他,因爲其實現是循環數組,所以插入不需要移動元素,效率也比較高。

2.LinkedBlockingQueue:當未設置容量時,是無解隊列,所以想用無界隊列,可以選用它,但是因爲隊列需要維護額外的指針,可能佔用的空間相對多一些。

2.其他隊列:其他隊列都有一些各自的特性功能,如果需要用到這些功能,則選用這些隊列。

  • 問題四:線程池中阻塞隊列設置多大合適?依據是什麼?

暫未理解,,後續分析,如果有知道的,歡迎評論..謝謝。

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