Java併發容器之LinkedBlockingQueue

    上一篇我們學習了ArrayBlockingQueue的實現原理,這一篇我們來學習與之對應的LinkedBlockingQueue。很明顯,ArrayBlockingQueue內部是基於數組實現的,而LinkedBlockingQueue是基於鏈表。他們都實現了阻塞隊列的take和put方法,下面我們會結合ArrayBlockingQueue作對比,來分析LinkedBlockingQueue的實現原理。

1 LinkedBlockingQueue簡介

    LinkedBlockingQueue是一個基於鏈表的,範圍任意的BlockingQueue。此隊列按照FIFO規則排序。隊列的頭部是在隊列中時間最長的元素。隊列的尾部 是在隊列中時間最短的元素。新元素插入到隊列的尾部,並且隊列獲取操作會獲得位於隊列頭部的元素。鏈接隊列的吞吐量通常要高於基於數組的隊列,但是在大多數併發應用程序中,其可預知的性能要低。
    可選的容量範圍構造方法參數作爲防止隊列過度擴展的一種方法。如果未指定容量,則它等於 Integer.MAX_VALUE。除非插入節點會使隊列超出容量,否則每次插入後會動態地創建鏈接節點。

2 LinkedBlockingQueue類圖結構

    1)int capacity :確定了隊列的容量,當指定容量後,不可超過。默認爲Integer.Max_VALUE。
    2)AtomicInteger count:隊列元素數量,採用原子整數,這裏與ArrayBlockingQueue不同(後者使用 int),因爲LinkedBlockingQueue的寫入。讀出操作使用了兩個不同的鎖,是可以並行操作的,因此需要原子類來保證更新的原子性。
    3)內部隊列實現使用的是Node<E>節點,與LinkedList相似。
    4)最重要一點,與ArrayBlockingQueue不同,那就是獲取和插入分成了兩個鎖。

3 構造方法

    
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
    構造函數主要是1)確定隊列大小,默認爲Integer.Max_VALUE;2)設置頭節點;3)設置尾節點。且頭尾節點一開始指向null。

4 添加元素

    offer方法

    將指定元素插入隊尾,成功返回true,方法返回。若隊列已滿,則插入失敗,返回false。
    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity)			//隊列已滿,則返回false
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);			//構造節點
        final ReentrantLock putLock = this.putLock;	//取得寫入鎖
        putLock.lock();
        try {
            if (count.get() < capacity) {		//需要在寫入之前再次判斷
                enqueue(node);				//入隊
                c = count.getAndIncrement();		//原子操作,防止讀線程競爭衝突
                if (c + 1 < capacity)
                    notFull.signal();			//如果未滿,通知寫入線程
            }
        } finally {
            putLock.unlock();				//解鎖
        }
        if (c == 0)					//如果寫入成功
            signalNotEmpty();				//通知讀取線程
        return c >= 0;
    }


    這裏討論下,爲什麼插入操作後也要通知寫入線程:因爲A、B線程同時寫,A獲得鎖,B被阻塞,因此,A完成後需要立刻通知B線程寫入,而不是等到讀取線程給B通知。

    put方法

    將指定元素插入到此隊列的尾部,如有必要,則等待空間變得可用。源碼如下:

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();					//如果中斷,則拋出異常並退出。避免了取得鎖後調用wait時才發現中斷
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {				//每次喚醒則獲取鎖 檢查容量  容量已滿則繼續阻塞
                notFull.await();
            }
            enqueue(node);						//容量未滿 插入操作
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();					//未滿 通知插入線程
        } finally {
            putLock.unlock();						//解鎖
        }
        if (c == 0)
            signalNotEmpty();						//通知讀取線程
    }

5 獲取元素

    peek方法

    獲取但不移除頭元素,若爲空返回null。源碼如下:

    public E peek() {
        if (count.get() == 0)				//元素爲空返回null
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();				//獲取鎖
        try {
            Node<E> first = head.next;			//頭結點的next即爲第一個結點
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

    peek方法從頭節點直接就可以獲取到第一個添加的元素,所以效率是比較高的。如果不存在則返回null。

    poll方法

    poll方法獲取並移除此隊列的頭,如果此隊列爲空,則返回 null。源碼如下:

    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;	//獲取鎖
        takeLock.lock();				//加鎖
        try {
            if (count.get() > 0) {			//如果有元素 則取出
                x = dequeue();
                c = count.getAndDecrement();		//更新count
                if (c > 1)
                    notEmpty.signal();			//通知其他讀取線程
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();				//通知寫線程
        return x;
    }

    take方法

    獲取並移除此隊列的頭部,在元素變得可用之前一直等待(如果有必要)。源碼如下:

    public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;	//獲取鎖
        takeLock.lockInterruptibly();			//加鎖
        try {
            while (count.get() == 0) {			//若已經空,則阻塞
                notEmpty.await();
            }
            x = dequeue();				//出隊
            c = count.getAndDecrement();		
            if (c > 1)					//若隊列中還有元素 則喚醒其他讀取線程
                notEmpty.signal();
        } finally {
            takeLock.unlock();				//解鎖
        }
        if (c == capacity)				//若不滿 喚醒插入線程
            signalNotFull();
        return x;
    }

6 總結

    ArrayBlockingQueue和LinkedBlockingQueue的不同

    1)內部實現不同

    ArrayBlockingQueue使用數組,而LinkedBlockingQueue使用鏈表。

    2)鎖的實現不同

    ArrayBlockingQueue入隊出隊都使用同一把鎖,而LinkedBlockingQueue使用了兩把鎖。因此,ArrayBlockingQueue的入隊出隊操作是同步的,而LinkedBlockingQueue是可以並行的。這裏的根本原因是:插入操作時,LinkedBlockingQueue的head不會影響head節點,而出隊操作也不會影響tail節點。所以可以並行。

    3)初始化條件不同

    ArrayBlockingQueue需要確定隊列大小而LinkedBlockingQueue不需要,具有默認值Integer.Max_VALUE。

    4)性能

    由於內部實現的不同,具有兩把鎖的LinkedBlockingQueue擁有較大的吞吐量,但是在大多數情況下,性能要低於ArrayBlockingQueue。
    

發佈了222 篇原創文章 · 獲贊 4 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章