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

隊列:生產消費模式及線程池的運用

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

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

通常兩種策略:github

  • 直接拒絕任務請求;
  • 將請求排隊,等有空閒線程的時候取出排隊的請求繼續處理。

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

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

完整代碼詳見 GitHub:https://github.com/UniqueDong/algorithms.git瀏覽器

什麼是隊列

用一個生活例子,能夠想象成超市排隊結帳,先來的先結帳,後面的人只能站在末尾,不容許插隊。先進先出,這就是所謂的「隊列」緩存

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

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

做爲基礎的數據結構,隊列的應用也很普遍,尤爲是一些特定場景下的隊列。好比循環隊列、阻塞隊列、併發隊列。它們在不少偏底層系統、框架、中間件的開發中,起着關鍵性的做用。好比高性能隊列 Disruptor、Linux 環形緩存,都用到了循環併發隊列;Java concurrent 併發包利用 ArrayBlockingQueue 來實現公平鎖等。框架

隊列與棧

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

順序隊列與鏈式隊列

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

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

順序隊列

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

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

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

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

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

如今咱們執行出隊操做

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

當咱們調用兩次出隊操做以後,隊列中 head 指針指向下標爲 2 的位置,tail 指針仍然指向下標爲 4 的位置。

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

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

具體代碼以下

/**
 * 數組實現隊列
 */
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 依次入隊以後,循環隊列中的元素就變成了下面的樣子:

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

隊列爲空的判斷依然是 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.棧實現瀏覽器的前進後退

原創不易,以爲有用但願隨手「在看」「收藏」「轉發」三連。

相關文章
相關標籤/搜索