9.隊列:生產者消費者模式

隊列:生產消費模式及執行緒池的運用

關注公眾號 MageByte,設置星標獲取最新乾貨。 「加群」 進入技術交流群獲更多技術成長。

向固定大小的執行緒池投放請求任務時,若果執行緒池中沒有空閑資源了,這時候還有新的請求進來,執行緒池如何處理這個請求?拒絕請求還是排隊?使用怎樣的處理機制

一般兩種策略:

  • 直接拒絕任務請求;
  • 將請求排隊,等有空閑執行緒的時候取出排隊的請求繼續處理。

那如何存儲排隊的請求呢?這就是今天要講的話題。

其底層的數據結構就是今天我們要講的內容,「隊列」Queue

完整程式碼詳見 GitHub://github.com/UniqueDong/algorithms.git

什麼是隊列

用一個生活例子,可以想像成超市排隊結賬,先來的先結賬,後面的人只能站在末尾,不允許插隊。先進先出,這就是所謂的「隊列」

隊列是一種線性數據結構,隊列的出口端叫「隊頭」,隊列的入口端叫「隊尾」。

與棧類似隊列的數據結構可以使用數組實現也可以使用鏈表實現。關於棧的內容同學們可以翻閱歷史文章學習「棧:實現瀏覽器前進後退」,隊列最基本的操作也是兩個:入隊 (enqueue) ,將新元素放到隊尾;出隊 (dequeue),從隊頭移除元素,出隊元素的下一個元素變成新的隊頭。

作為基礎的數據結構,隊列的應用也很廣泛,尤其是一些特定場景下的隊列。比如循環隊列、阻塞隊列、並發隊列。它們在很多偏底層系統、框架、中間件的開發中,起著關鍵性的作用。比如高性能隊列 Disruptor、Linux 環形快取,都用到了循環並發隊列;Java concurrent 並發包利用 ArrayBlockingQueue 來實現公平鎖等。

隊列與棧

隊列也是一種操作受限的線性表數據結構。

順序隊列與鏈式隊列

隊列是跟棧一樣,是一種抽象的數據結構。 具有先進先出的特性,在隊頭刪除數據,在隊尾插入數據。

可以使用數組實現,也可以使用鏈表實現。使用數組實現的叫 順序隊列,用鏈表實現的 叫 鏈式隊列

順序隊列

一起先來看數組實現的隊列:

  1. 出隊操作就是把元素移除隊列,只允許在隊頭移除,出隊的下一個元素成為新的隊頭。
  2. 入隊操作就是把新元素放入隊列,只允許在隊尾插入,新元素的的下一個位置成為隊尾。

隨著不停地進行入隊、出隊操作,head 和 tail 都會持續往後移動。當 tail 移動到最右邊,即使數組中還有空閑空間,也無法繼續往隊列中添加數據了。這個問題該如何解決呢?

當出現這種情況的時候我們就需要做數據遷移。如圖所示:當 abcd 入隊後,對應的指針位置。

現在我們執行出隊操作

當我們調用兩次出隊操作之後,隊列中 head 指針指向下標為 2 的位置,tail 指針仍然指向下標為 4 的位置。

遷移操作其實就是把整段數據移動到數組 0 開始的位置。

具體程式碼如下

/**
 * 數組實現隊列
 */
public class ArrayQueue<E> extends AbstractQueue<E> {
    /**
     * The queued items
     */
    final E[] items;
    /**
     * 隊頭指針
     */
    private int front;

    /**
     * 隊尾指針
     */
    private int rear;

    /**
     * Creates an ArrayQueue with the given capacity
     *
     * @param capacity the capacity of this queue
     */
    public ArrayQueue(Class<E> type, int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException();
        }
        this.items = (E[]) Array.newInstance(type, capacity);
    }


    public int capacity() {
        return items.length;
    }

    @Override
    public E dequeue() {
        if (front == rear) {
            throw new IllegalStateException("Queue empty");
        }
        return items[front++];
    }

    @Override
    public boolean enqueue(E e) {
        if (isFull()) {
            throw new IllegalStateException("Queue empty");
        }
        // 隊尾沒有空間了,需要執行數據遷移
        if (rear == capacity()) {
            // 數據遷移
            if (rear - front >= 0)  {
                System.arraycopy(items, front, items, 0, rear - front);
            }
            // 調整 front 與 rear
            rear -= front;
            front = 0;
        }
        items[rear++] = e;
        return true;
    }

    @Override
    public boolean isFull() {
        return rear == capacity() && front == 0;
    }

    @Override
    public boolean isEmpty() {
        return front == rear;
    }
}

鏈式隊列

我們可以通過之前學習過的鏈表來實現隊列,具體詳見單向鏈表篇 。其實主要就是利用了 出隊就是鏈表頭刪除數據,入隊就是尾節點添加數據

public class LinkedQueue<E> extends AbstractQueue<E> implements Queue<E> {

    private final SingleLinkedList<E> linkedList;

    public LinkedQueue() {
        this.linkedList = new SingleLinkedList<>();
    }

    @Override
    public E dequeue() {
        if (linkedList.isEmpty()) {
            throw new IllegalStateException("Queue empty");
        }
        return linkedList.remove();
    }

    @Override
    public boolean enqueue(E e) {
        return linkedList.add(e);
    }

    @Override
    public boolean isFull() {
        return false;
    }

    @Override
    public boolean isEmpty() {
        return linkedList.isEmpty();
    }
}

循環隊列

剛剛的例子,當 rear == capacity 的時候,會出現數據遷移操作,這樣性能受到影響,那如何避免呢?

原本數組是有頭有尾的,是一條直線。現在我們把首尾相連,扳成了一個環。

環形隊列

我們可以看到,圖中這個隊列的大小為 8,當前 head=4,tail=7。當有一個新的元素 a 入隊時,我們放入下標為 7 的位置。但這個時候,我們並不把 tail 更新為 8,而是將其在環中後移一位,到下標為 0 的位置。當再有一個元素 b 入隊時,我們將 b 放入下標為 0 的位置,然後 tail 加 1 更新為 1。所以,在 a,b 依次入隊之後,循環隊列中的元素就變成了下面的樣子:

隊列為空的判斷依然是 front == rear,隊列滿的條件則是 (rear + 1) % capacity = front

你有沒有發現,當隊列滿時,圖中的 tail 指向的位置實際上是沒有存儲數據的。所以,循環隊列會浪費一個數組的存儲空間。

/**
 * 數組實現環形隊列
 *
 * @param <E>
 */
public class ArrayCircleQueue<E> extends AbstractQueue<E> {

    /**
     * The queued items
     */
    final E[] items;
    /**
     * 隊頭指針
     */
    private int front;

    /**
     * 隊尾指針
     */
    private int rear;

    public int capacity() {
        return items.length;
    }

    /**
     * Creates an ArrayQueue with the given capacity
     *
     * @param capacity the capacity of this queue
     */
    public ArrayCircleQueue(Class<E> type, int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException();
        }
        this.items = (E[]) Array.newInstance(type, capacity);
    }

    @Override
    public E dequeue() {
        if (front == rear) {
            throw new IllegalStateException("Queue empty");
        }
        E item = items[front];
        front = (front + 1) % items.length;
        return item;
    }

    @Override
    public boolean enqueue(E e) {
        checkNotNull(e);
        int newRear = (rear + 1) % items.length;
        if (newRear == front) {
            throw new IllegalStateException("Queue full");
        }
        items[rear] = e;
        this.rear = newRear;
        return true;
    }

    @Override
    public boolean isFull() {
        return (rear + 1) % items.length == front;
    }

    @Override
    public boolean isEmpty() {
        return rear == front;
    }
}

MageByte

推薦閱讀

1.跨越數據結構與演算法

2.時間複雜度與空間複雜度

3.最好、最壞、平均、均攤時間複雜度

4.線性表之數組

5.鏈表導論-心法篇

6.單向鏈表正確實現方式

7.雙向鏈表正確實現

8.棧實現瀏覽器的前進後退

原創不易,覺得有用希望隨手「在看」「收藏」「轉發」三連。