java併發面試常識之ArrayBlockingQueue

xpbob發表於2018-06-11

ArrayBlockingQueue是常用的執行緒集合,線上程池中也常常被當做任務佇列來使用。使用頻率特別高。他是維護的是一個迴圈佇列(基於陣列實現),迴圈結構在資料結構中比較常見,但是在原始碼實現中還是比較少見的。

執行緒安全的實現

執行緒安全佇列,基本是離不開鎖的。ArrayBlockingQueue使用的是ReentrantLock,配合兩種Condition,實現了集合的執行緒安全操作。這裡稍微說一個好習慣,下面是成員變數的宣告。


    private static final long serialVersionUID = -817911632652898426L;
    final Object[] items;
    int takeIndex;
    int putIndex;
    int count;
    final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;
    transient Itrs itrs = null;

賦值的操作基本都是在建構函式裡做的。這樣有個好處,程式碼執行可控。成員變數的初始化也是會合並在構造方法裡執行的,但是在執行順序上需要好好斟酌,如果寫在構造方法裡初始化,則沒有相關問題。

阻塞佇列的常用場所就是生產者消費者。一般都是生產者放入,消費者從頭取資料。下面重點說這兩個操作。
這兩個操作都是依靠鎖來保證執行緒安全的。

生產操作

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

put等放入操作,首先是獲取鎖,如果發現資料滿了,就通過notFull的condition,來阻塞執行緒。這裡的條件判定一定是用while而不是if,多執行緒情況下,可以被喚醒後發現又滿了。

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

這個是入佇列的操作。首先獲取維護的陣列。putindex就是放入操作的標誌。這個操作會一直加。達到預定的長度後就變成0從頭開始計數。這樣插入的操作就是一個迴圈的操作了,count就是用來做計數的,作為能否插入資料的一個標準,插入資料後就通過notEmpty的condition發出一個訊號喚醒消費執行緒。

消費操作

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

  

消費的方法也是這樣。先獲取鎖,然後進行條件判斷,如果沒有資料,則阻塞執行緒。注意點和put一樣。

 private E dequeue() {
        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,這是一個標誌,這個數值也會一直增加,表示取的第一個資料的位置。如果這個標誌走到最後,然後變成0,從頭再來。這樣保證取出的資料都是fifo的順序。刪除的時候如果發現迭代中,則會修改迭代器的遍歷。然後通過notFull的condition來喚醒生產執行緒。

移除操作

    public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

對於remove操作就比較麻煩了,首先獲取鎖之後,把兩個標誌位本地化,然後找到要刪除的元素的位置。呼叫removeAt,這裡刪除需要對標誌位做改變。

    void removeAt(final int removeIndex) {
        final Object[] items = this.items;
        if (removeIndex == takeIndex) {
            items[takeIndex] = null;
            if (++takeIndex == items.length)
                takeIndex = 0;
            count--;
            if (itrs != null)
                itrs.elementDequeued();
        } else {
            final int putIndex = this.putIndex;
            for (int i = removeIndex;;) {
                int next = i + 1;
                if (next == items.length)
                    next = 0;
                if (next != putIndex) {
                    items[i] = items[next];
                    i = next;
                } else {
                    items[i] = null;
                    this.putIndex = i;
                    break;
                }
            }
            count--;
            if (itrs != null)
                itrs.removedAt(removeIndex);
        }
        notFull.signal();
    }

如果刪除的元素是位置和takeindex一樣。那就可以直接刪除,然後讓刪除標誌位向後移動。如果不是,則從刪除的位置開始,進行後面向前面的資料覆蓋的操作。直到遇到putindex的前一個位置。然後把那個位置的資料設定為null。並且把putindex的位置往前移動一格,正在迭代的時候要刪除資料並且喚醒生產執行緒。


相關文章