Java併發——阻塞佇列集(上)

午夜12點發表於2018-08-15

簡介

阻塞佇列是一個支援兩個附加操作的佇列,這兩個附加操作支援阻塞的插入和移除方法
①.支援阻塞的插入方法:當佇列滿時,佇列會阻塞插入元素的執行緒,直至佇列不滿
②.支援阻塞的移除方法:當佇列空時,獲取元素的執行緒會等待佇列變為非空

在阻塞佇列不可用時,這兩個附加操作提供了4種處理方式,如下

方法/處理方式 丟擲異常 返回特殊值 一直阻塞 超時退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
檢查方法 element() peek() 不可用 不可用

阻塞佇列

ArrayBlockingQueue:由陣列結構組成的有界阻塞佇列
LinkedBlockingQueue:由連結串列結構組成的有界阻塞佇列
PriorityBlockingQueue:支援優先順序排序的無界阻塞佇列
DelayQueue:使用優先順序佇列實現的無界阻塞佇列
SynchronousQueue:不儲存元素的阻塞佇列
LinkedTransferQueue:由連結串列結構組成的無界阻塞佇列
LinkedBlockingDeque:由連結串列結構組成的雙向阻塞佇列

ArrayBlockingQueue

ArrayBlockingQueue是一個用陣列實現的有界阻塞佇列,佇列按照先進先出(FIFO)原則對元素進行排序。預設採用不公平訪問,因為公平性通常會降低吞吐量。

主要屬性


    private static final long serialVersionUID = -817911632652898426L;
    /** 陣列用來維護ArrayBlockingQueue中的元素 */
    final Object[] items;
    /** 出隊首位置索引 */
    int takeIndex;
    /** 入隊末位置索引 */
    int putIndex;
    /** 元素個數 */
    int count;
    
    final ReentrantLock lock;
    /** 出隊等待佇列 */
    private final Condition notEmpty;
    /** 入隊等待佇列 */
    private final Condition notFull;
複製程式碼

put

ArrayBlockingQueue提供了很多方法入隊:add()、offer()、put()等。我們以阻塞式方法為主,put()方法其原始碼如下


    public void put(E e) throws InterruptedException {
        // 校驗元素是否為空
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        // 響應中斷式獲取同步,若執行緒被中斷會丟擲異常
        lock.lockInterruptibly();
        try {
            // 當佇列已滿,將執行緒新增到notFull等待佇列中
            while (count == items.length)
                notFull.await();
            // 若沒有滿,進行入隊    
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
複製程式碼

當佇列滿時,會呼叫Condition的await()方法將執行緒新增到等待佇列中。若佇列未滿呼叫enqueue()進行入隊操作(所有入隊方法最終都將呼叫該方法在佇列尾部插入元素


    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        // 入隊
        items[putIndex] = x;
        // 當陣列新增滿後,重新從0開始
        if (++putIndex == items.length)
            putIndex = 0;
        // 元素個數+1    
        count++;
        // 喚醒出隊等待佇列中的執行緒
        notEmpty.signal();
    }
複製程式碼

take

出隊方法有:poll()、remove(),take()等,take()方法其原始碼如下


    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 響應中斷式獲取同步,若執行緒被中斷會丟擲異常
        lock.lockInterruptibly();
        try {
            // 若佇列空,將執行緒新增到notEmpty等待佇列中
            while (count == 0)
                notEmpty.await();
            // 獲取資料    
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
複製程式碼

當佇列為空,會呼叫condition的await()方法將執行緒新增到notEmpty等待佇列中,若佇列不為空則呼叫dequeue()獲取資料


    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;
        // 元素個數-1    
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 通知入隊等待佇列中的執行緒
        notFull.signal();
        return x;
    }
複製程式碼

從原始碼中可以發現ArrayBlockingQueue通過condition的等待喚醒機制完成可阻塞式的入隊和出隊

LinkedBlockingQueue

LinkedBlockingQueue是一個用連結串列實現的有界阻塞佇列。此佇列的預設和最大長度為 Integer.MAX_VALUE。此佇列按照先進先出的原則對元素進行排序

主要屬性


    /** 容量 */
    private final int capacity;
    /** 元素個數 */
    private final AtomicInteger count = new AtomicInteger();
    /** 頭節點 */
    transient Node head;
    /** 尾節點 */
    private transient Node last;
    /** 出隊鎖 */
    private final ReentrantLock takeLock = new ReentrantLock();
    /** 出隊等待佇列 */
    private final Condition notEmpty = takeLock.newCondition();
    /** 入隊鎖 */
    private final ReentrantLock putLock = new ReentrantLock();
    /** 入隊等待佇列 */
    private final Condition notFull = putLock.newCondition();
複製程式碼

從屬性上來看LinkedBlockingQueue維護兩個鎖在入隊和出隊時保證執行緒安全,兩個鎖降低執行緒由於執行緒無法獲取lock而進入WAITING狀態的可能性提高了執行緒併發執行的效率,並且count屬性使用AtomicInteger原子操作類(可能兩個執行緒一個出隊一個入隊操作count,各自的鎖顯然起不到用處)

put


    public void put(E e) throws InterruptedException {
        // 若新增元素為null拋異常
        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 node = new Node(e);
        final ReentrantLock putLock = this.putLock;
        // 獲取當前元素個數
        final AtomicInteger count = this.count;
        // 響應中斷式獲取鎖,若執行緒被中斷會丟擲異常
        putLock.lockInterruptibly();
        try {
            // 若當前佇列已滿,將執行緒新增到notFull等待佇列中
            while (count.get() == capacity) {
                notFull.await();
            }
            // 若沒有滿,進行入隊
            enqueue(node);
            // 元素個數+1
            c = count.getAndIncrement();
            // 若當前元素個數+1還未到定義的最大容量,則喚醒入隊等待佇列中的執行緒
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
 複製程式碼

take


    public E take() throws InterruptedException {
        E x;
        int c = -1;
        // 獲取當前元素個數
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        // 響應中斷式獲取鎖,若執行緒被中斷會丟擲異常
        takeLock.lockInterruptibly();
        try {
            // 若當前佇列為空,則將執行緒新增到notEmpty等待佇列中
            while (count.get() == 0) {
                notEmpty.await();
            }
            // 獲取資料
            x = dequeue();
            // 當前元素個數-1
            c = count.getAndDecrement();
            // 若佇列中還有元素,喚醒阻塞的出隊執行緒
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }
 複製程式碼

PriorityBlockingQueue

PriorityBlockingQueue是一個支援優先順序的無界阻塞佇列,雖然無界但由於資源耗盡,嘗試的新增可能會失敗(導致OutOfMemoryError ),預設情況下元素採取自然順序升序排序,也可以通過建構函式來指定Comparator來對元素進行排序,需要注意的是PriorityBlockingQueue不能保證同優先順序元素的順序

主要屬性


    /** 預設容量 */
    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    /** 最大容量 */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    /** 內建陣列 */
    private transient Object[] queue;
    /** 元素個數 */
    private transient int size;
    /** 比較器,為空則自然排序 */
    private transient Comparator comparator;
    
    private final ReentrantLock lock;
    /** 出隊等待佇列 */
    private final Condition notEmpty;
    /** 用於CAS擴容時用 */
    private transient volatile int allocationSpinLock;

    private PriorityQueue q;
複製程式碼

可以發現PriorityBlockingQueue只有一個condition,因為PriorityBlockingQueue是一個無界佇列,插入始終成功,也正因為此所以其入隊用lock.lock()方法不響應中斷,而出隊用lock.lockInterruptibly()響應中斷式獲取鎖

put


    public void put(E e) {
        // 不需要阻塞
        offer(e); // never need to block
    }
    
    public boolean offer(E e) {
        // 判空
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        // 獲取鎖
        lock.lock();
        int n, cap;
        Object[] array;
        // 若大於等於當前陣列長度則擴容
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            // 獲取比較器
            Comparator cmp = comparator;
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            // 元素個數+1    
            size = n + 1;
            // 喚醒
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }
複製程式碼

tryGrow擴容


    private void tryGrow(Object[] array, int oldCap) {
        // 必須先釋放鎖
        lock.unlock(); // must release and then re-acquire main lock
        Object[] newArray = null;
        // CAS設定佔用
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                // 新容量
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                //  新容量若超過最大值                  
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                // 若新容量大於舊容量且當前陣列相等,建立新容量陣列
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }
        // CAS設定allocationSpinLock失敗,表明有其他執行緒也正在擴容,讓給其他執行緒處理
        if (newArray == null) // back off if another thread is allocating
            Thread.yield();
        // 獲取鎖    
        lock.lock();
        if (newArray != null && queue == array) {
            queue = newArray;
            // 陣列複製
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }
複製程式碼

從原始碼中可以發現為了儘可能提高併發效率,先釋放鎖在計算新容量時利用CAS設定allocationSpinLock來保證執行緒安全,再最後獲取鎖進行陣列複製擴容。擴容完後,根據比較器的排序規則進行新增

siftUpComparable(),比較器comparator為null時採取自然排序呼叫此方法


    private static  void siftUpComparable(int k, T x, Object[] array) {
        Comparable key = (Comparable) x;
        // 若當前元素個數大於0,即佇列不為空
        while (k > 0) {
            // (n - 1) / 2
            int parent = (k - 1) >>> 1;
            // 獲取parent位置上的元素
            Object e = array[parent];
            // 從佇列的最後往上調整堆,直到不小於其父節點為止
            if (key.compareTo((T) e) >= 0)
                break;
            // 如果當前節點小於其父節點,則將其與父節點進行交換,並繼續往上訪問父節點    
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }
複製程式碼

此方法為建堆過程,假定PriorityBlockingQueue內部陣列如下:

Java併發——阻塞佇列集(上)
轉換為堆(堆是一種二叉樹結構):

Java併發——阻塞佇列集(上)
往其新增元素2,k為當前元素個數12,計算parent為5,e為6,e大於2,交換位置

Java併發——阻塞佇列集(上)

第二次迴圈,k=5,parent=2,e=5,5>2交換位置

Java併發——阻塞佇列集(上)
第三次迴圈,k=2,parent=0,e=1,1<2退出迴圈,第2個位置給新元素2

其主要思路 末位置尋找其父節點,若新增元素小於父節點則將其與父節點進行交換,並繼續往上訪問父節點,直到大於等於其父節點為止

siftUpUsingComparator(),當比較器不為null,採用指定比較器,呼叫此方法


    private static  void siftUpUsingComparator(int k, T x, Object[] array,
                                       Comparator cmp) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (cmp.compare(x, (T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = x;
    }
複製程式碼

take


    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        E result;
        try {
            while ( (result = dequeue()) == null)
                notEmpty.await();
        } finally {
            lock.unlock();
        }
        return result;
    }
複製程式碼

獲取鎖後,呼叫dequeue()


    private E dequeue() {
        // 若佇列為空,返回null
        int n = size - 1;
        if (n < 0)
            return null;
        else {
            Object[] array = queue;
            // 出隊元素,首元素
            E result = (E) array[0];
            // 最後一個元素
            E x = (E) array[n];
            array[n] = null;
            Comparator cmp = comparator;
            if (cmp == null)
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }
複製程式碼

自然排序處理siftDownComparable()


    private static  void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable key = (Comparable)x;
            int half = n >>> 1;           // loop while a non-leaf
            while (k < half) {
                // 左節點
                int child = (k << 1) + 1; // assume left child is least
                Object c = array[child];
                // 右節點
                int right = child + 1;
                if (right < n &&
                    ((Comparable) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                if (key.compareTo((T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = key;
        }
    }
複製程式碼

指定排序siftDownUsingComparator()


    private static  void siftDownUsingComparator(int k, T x, Object[] array,
                                                    int n,
                                                    Comparator cmp) {
        if (n > 0) {
            int half = n >>> 1;
            while (k < half) {
                int child = (k << 1) + 1;
                Object c = array[child];
                int right = child + 1;
                if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
                    c = array[child = right];
                if (cmp.compare(x, (T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = x;
        }
    }
複製程式碼

以上面最後一個圖為基礎出隊第一個元素

Java併發——阻塞佇列集(上)
第一次迴圈:k=0,n=12,half=6,child=1,c為圖中節點3,right=2,經過子節點比較找出較小值2,2與末尾值節點6相比,末位置更大,首位置與右子節點交換位置

Java併發——阻塞佇列集(上)

第二次迴圈:k=2,child=5,c為圖中節點5,right=6,經過子節點比較找出較小值5,5與末位置節點6相比,末位置更大,與左子節點交換位置

Java併發——阻塞佇列集(上)

第三次迴圈:k=5,child=11,c為圖中節點8,right=12,經過子節點比較找出較小值末位置節點6相比

Java併發——阻塞佇列集(上)

其主要思路:首位置尋找其子節點,找出兩個子節點的較小的與末尾位置節點比較若末尾節點小,則將其置入首位置,否則首位置與較小子節點替換位置,以此略推繼續往下找

DelayQueue

DelayQueue是一個支援延時獲取元素的無界阻塞佇列,佇列使用PriorityQueue來實現。佇列中的元素必須實現Delayed介面,在建立元素時可以指定多久才能從佇列中獲取當前元素,只有在延遲期滿時才能從佇列中提取元素,可以將其應用在快取、定時任務排程等場景

Delayed介面

DelayQueue佇列中的元素必須實現Delayed介面,我們先看Delayed介面繼承關係

Java併發——阻塞佇列集(上)

從圖中我們可以知道,實現Delayed介面,我們必須實現其自定義的getDelay()方法以及繼承過來的compareTo()方法

主要屬性


    private final transient ReentrantLock lock = new ReentrantLock();
    /** 優先順序佇列 */
    private final PriorityQueue q = new PriorityQueue();

    private Thread leader = null;

    private final Condition available = lock.newCondition();
複製程式碼

put


    public void put(E e) {
        offer(e);
    }
    
    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // 向PriorityQueue新增元素
            q.offer(e);
            // 若當前元素
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }
複製程式碼

其新增操作基於PriorityQueue的offer方法


    public boolean offer(E e) {
        // 判空
        if (e == null)
            throw new NullPointerException();
        // 修改次數
        modCount++;
        int i = size;
        // 判斷是否需要擴容
        if (i >= queue.length)
            grow(i + 1);
        // 元素個數+1    
        size = i + 1;
        // 若佇列為空,首元素置為e
        if (i == 0)
            queue[0] = e;
        else
            siftUp(i, e);
        return true;
    }
    
    private void siftUp(int k, E x) {
        if (comparator != null)
            siftUpUsingComparator(k, x);
        // 自然排序
        else
            siftUpComparable(k, x);
    }
    
    /**
     * 自然排序
     */
    private void siftUpComparable(int k, E x) {
        Comparable key = (Comparable) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }
    
    /**
     * 指定比較器
     */
    private void siftUpUsingComparator(int k, E x) {
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (comparator.compare(x, (E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = x;
    }
複製程式碼

PriorityQueue的自然排序或指定比較器處理新增操作與PriorityBlockingQueue的邏輯差不多,這裡就不再過多分析,但是從原始碼我們發現了modCount,表明PriorityQueue是執行緒不安全的,但是由於DelayQueue可以依靠ReentrantLock來確保同步安全。新增完後會判斷新增元素是否為佇列首元素,若是將leader設定為空,並喚醒所有等待執行緒

take


    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            // 死迴圈
            for (;;) {
                // 獲取佇列首元素,若佇列為空返回null
                E first = q.peek();
                // 若佇列為空
                if (first == null)
                    available.await();
                else {
                    // 獲取剩餘延遲時間
                    long delay = first.getDelay(NANOSECONDS);
                    // 若小於0表明已過期,出隊
                    if (delay <= 0)
                        return q.poll();
                    first = null; // don't retain ref while waiting
                    // 若leader!= null 表明有其他執行緒正在操作
                    if (leader != null)
                        available.await();
                    else {
                        // 否則將leader置為當前執行緒
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            // 指定時間等待
                            available.awaitNanos(delay);
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }
複製程式碼

整體出隊邏輯不再多述,來說下leader和first

  • leader
  • 從原始碼我們可以看到leader屬性在put()與take()方法中都有出現,其作用在於減少不必要的競爭,若leader不為空說明已經有執行緒正在操作,直接一直等待即可沒必要再爭。舉個例子假定有執行緒A、B、C依次要出隊,執行緒A先獲取鎖由於首元素未過期,指定剩餘時間等待,若不採用leader直接一直等待,執行緒B和C也指定時間等待,那麼會造成三個執行緒同時競爭首元素,本來A→B→C的順序可能導致亂序不是執行緒所想要的元素

  • first
  • 在take()方法中為什麼要將first置為null,英文註解當等待時不要持有依賴。若不置空假定執行緒A等待,其棧幀中會存有first區域性變數所指元素引用,執行緒B請求仍然等待其棧幀也存有first區域性變數所指元素引用,以此略推後來執行緒等待後棧幀中都會存有,那麼當執行緒A成功出隊首元素,其他執行緒依然佔有其引用,導致一直回收不了,這樣就可能會造成記憶體洩漏

    感謝

    《java併發程式設計的藝術》
    cmsblogs.com/?p=2407

    相關文章