jdk源碼之java集合類(三)——Queue家族

上一篇文章已經介紹了List的家族,今天就來看一看另一個數據類型Queue的大家族罷!

我們先看一看Queue家族的主要成員的類族結構(這裏並不是所有的Queue都羅列的,不然太多了)

從圖中我們可以看出,除了線性集合的統一接口Collection外,Queue家族總共有四個主要接口:Queue、Deque、BlockingQueue、BlockingDeque,四個接口相互繼承,構成了Queue家族的基本體系。

此外,Queue有一個抽象實現類AbstractQueue,而Queue家族的幾乎所有實現類全部繼承此類,此類完成了些許統一操作,子類各自會實現不同接口以及擁有各自的獨立操作。

值得一提的是,Deque的兩個直接實現類並不繼承這個抽象類,個人認爲有兩方面原因:1.語法限制,接口的繼承雖然可以多元化,但是其實現類的繼承只能做出單一的選擇,而BlockingDeque的實現類與其去繼承Deque的實現類,不如去繼承BlockingQueue的;2.Deque的一個實現類LinkedList原本不屬於Queue家族,只是由於其實現方式恰好可以方便於隊列操作,因此纔有LinkedList實現Deque的設計。關於LinkedList的實現,請轉至上一篇博客jdk源碼之java集合類(二)——List家族

接下來我們就根據源碼一步步地講述Queue家族成員的作用和實現。

一、接口

我們先看下Queue家族的四個接口分別幹啥的。

Queue:Queue家族的基本接口,定義了一系列隊列結構需要的操作:

熟悉隊列這種數據結構的童鞋應該對這幾個方法的定義沒有太大疑問吧。隊列基本包含三種操作:插入數據,獲取第一個數據,獲取並刪除第一個數據。而這裏對於這三種操作都分別定義了兩種不同的操作方式,如下圖:

官方的解釋是這樣的:在處理元素前用於保存元素的 collection。除了基本的 Collection 操作外,隊列還提供其他的插入、提取和檢查操作。每個方法都存在兩種形式:一種拋出異常(操作失敗時),另一種返回一個特殊值(null 或 false,具體取決於操作)。插入操作的後一種形式是用於專門爲有容量限制的 Queue 實現設計的;在大多數實現中,插入操作不會失敗。

說的很抽象,反正只要知道,作爲隊列使用時,優先選擇返回特殊值的那種就行了。

然後是Deque,這個數據結構代表的是雙向隊列,隊列的進出並不只是單向的。既然數據的操作形式變成了雙向的,那操作方法的定義自然也要變爲原來的兩倍:

這個我感覺並不需要解釋。

然後是二者的Blocking。顧名思義,這兩個接口定義了帶有阻塞的隊列。我們先來看BlockingQueue:

我們看到,在這個接口中有兩個非常重要的操作被加入:一個在定長的隊列滿時加入阻塞隊列,如果超時就丟棄;一個在隊列爲空的時候等待其他線程進行入隊操作(blocking的都是線程安全的),如果超時就不再獲取。

這種模式非常適用於消費者-生產者模型的隊列,這是一種官方說法,如果參考jdk自帶的線程池中,BlockingQueue的使用(我之前的博客jdk源碼之線程池)的話,或許會容易理解一些。在線程池中,任務的入隊和啓動線程去執行任務並非同步的,由於線程池的大小有限,當線程池內所有線程都處於工作狀態時,任務就會進入阻塞隊列,而當線程池中有一個任務被執行完畢以後,其所屬線程就回去阻塞隊列中獲取新的任務處理,這種阻塞被賦予了時間限制,這裏就採用了BlockingQueue的特性。

至於BlockingDeque,他提供的操作和BlockingQueue類似,只不過由於其雙向結構的特性,操作更多一倍而已。

 

二、抽象類

接下來我們看一下Queue家族的抽象類,AbstractQueue的實現:

還記得在上一篇中我們提到的抽象類的設計原則嘛?通常我們將操作中需要互相調用的那部分在抽象類中完成,而最基本(也可能是參數最多)的那個操作則被各個子類實現(因爲各個子類的方式各異)。

在Queue接口中,我們說過最主要的操作其實是三組,而三組操作又各有兩種方法被定義,根據我們所述的原則,這三組操作應該各自留下一個方法交給子類實現,而另外的方法則是調用被留下的尚未實現的方法去實現其基本操作。

從Queue的方法圖中我們瞭解到,add和offer類似,但是add拋出了異常,而在抽象類實現時,offer留給了子類實現,add則是調用offer。

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

同理,remove和get也基本如此。這裏就不貼代碼了。

接下來我們選擇性地來看一看這個抽象類的幾個實現類。(不全部看了太多了)

三、ArrayBlockingQueue

這是一個實現了BlockingQueue,即阻塞隊列接口的實現類。

來看其offer方法:

    public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

由於BlockingQueue必須是線程安全的,所以在實現的時候使用了ReentrantLock來加鎖(關於這個鎖,我很久前在java鎖機制一文中描述過,但個人感覺不是非常詳細,更加沒有往源碼層面做過多探討,所以之後可能會專門搞一篇來探究一下這個鎖中的奧祕);在操作前加鎖以後,判斷長度是否超出,若無,入隊,若有,返回false;結束後開鎖。

來看看enqueue方法:

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

這個方法總體而言比較簡單,隊列的列表存在一個items數組中,將其拿來,進行入隊操作,完成後將count加一。

以上是offer方法的基本實現,而poll、peek等操作最終調用的方法分別是dequeue和itemAt方法,篇幅有限,調用過程不做詳解,只展示一下dequeue和itemAt方法:

    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

方法首先獲取隊首(takeIndex記錄的位置),然後將隊首設置爲空,然後返回隊首,這裏還有個操作是將迭代器也進行一次出隊操作(itrs.elementDequeued)

再看itemAt方法:

    @SuppressWarnings("unchecked")
    final E itemAt(int i) {
        return (E) items[i];
    }

較爲簡單。

然後再來看看BlockingQueue特有的阻塞操作:

    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 {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

此方法比一般的offer方法多了時間參數,因此當count等於隊列最大長度的時候,方法並不直接返回false,而是循環阻塞等待,如果在等待時間結束前count變小,則進行入隊操作enqueue(e),否則返回false。

    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) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

帶有時間參數的poll方法也類似,在隊列爲空時阻塞獲取操作,直到隊列不再爲空或等待時間耗盡。

四、LinkedBlockingDeque

上一節講的是用Array的數據結構實現的Queue,這一節就講Linked結構實現Deque。這樣涵蓋量大些。

其實這個類的實現中許多細節都與LinkedList類似:

    /** Doubly-linked list node class */
    static final class Node<E> {
        /**
         * The item, or null if this node has been removed.
         */
        E item;

        /**
         * One of:
         * - the real predecessor Node
         * - this Node, meaning the predecessor is tail
         * - null, meaning there is no predecessor
         */
        Node<E> prev;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head
         * - null, meaning there is no successor
         */
        Node<E> next;

        Node(E x) {
            item = x;
        }
    }

定義Node;

    private boolean linkFirst(Node<E> node) {
        // assert lock.isHeldByCurrentThread();
        if (count >= capacity)
            return false;
        Node<E> f = first;
        node.next = f;
        first = node;
        if (last == null)
            last = node;
        else
            f.prev = node;
        ++count;
        notEmpty.signal();
        return true;
    }

在隊首加入元素。

    /**
     * Links node as last element, or returns false if full.
     */
    private boolean linkLast(Node<E> node) {
        // assert lock.isHeldByCurrentThread();
        if (count >= capacity)
            return false;
        Node<E> l = last;
        node.prev = l;
        last = node;
        if (first == null)
            first = node;
        else
            l.next = node;
        ++count;
        notEmpty.signal();
        return true;
    }

在隊尾加元素。

諸如此類的私有方法列了不少,其本質也就是一些鏈表結構的操作,在此不多敘述,我們來看看它所實現的接口中定義的那堆方法:

    public void addFirst(E e) {
        if (!offerFirst(e))
            throw new IllegalStateException("Deque full");
    }

    public boolean offerFirst(E e) {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return linkFirst(node);
        } finally {
            lock.unlock();
        }
    }

addFirst調用offerFirst(與抽象類中add調用offer一樣),而offerFirst則調用了私有方法linkFirst。

    public E removeLast() {
        E x = pollLast();
        if (x == null) throw new NoSuchElementException();
        return x;
    }

    public E pollLast() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return unlinkLast();
        } finally {
            lock.unlock();
        }
    }

removeLast調用了pollLast,同樣,pollLast也是調用私有方法unlinkLast。

其餘的在Deque中定義的基本方法的實現方式就不再詳述了,基本都類似。

然後與BlockingQueue的實現類一樣,我們也要看一看該類中如何處理阻塞:

    public boolean offerLast(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (!linkLast(node)) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

這裏照樣也使用了TimeUnit的Nanos來完成時間等待,只是在判斷長度的過程中不再使用count比較,而是直接調用linkLast,根據返回值來判斷,這也是由於鏈表結構在增刪過程中時間複雜度相對較低而做出的選擇。

小結

我們把Queue家族的接口和主要成員做了介紹,並閱讀了兩三個類的實現源碼,基本上對於Queue的實現細節有了較爲清晰的認識。

閱讀過程中略過了一些較爲難懂的樂觀鎖、原子類等方面的操作細節,對此會在以後的關於java鎖方面的專題文章中做出進一步的解讀。

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