Java源碼:阻塞隊列(ArrayBlockingQueue)

一、簡介

所謂阻塞隊列,其實就是支持下面這兩種阻塞功能的隊列:

  • 當隊列爲空時,讀取該隊列可以阻塞直到隊列不爲空;
  • 當隊列已滿時,寫入該隊列可以阻塞直到隊列不爲滿;

這種阻塞隊列主要用於可以用來構建生產者-消費者模型,生產者只需要往隊列中發送消息,而消費者也只需要專注於從隊列中讀取消息,剩下的同步、阻塞細節都交給阻塞隊列把。


Java提供了下面7種阻塞隊列,區別於底層數據結構的不同:

  • ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
  • LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
  • PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
  • DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
  • LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
阻塞隊列的接口是:java.util.concurrent.BlockingQueue,主要提供了以下存取方法(根據隊列空或者滿時的響應方式,可分成3類):
  立即返回結果值 超時返回結果值 阻塞 拋出異常
插入 offer(e) offer(e,time,unit) put(e) add(e)
移除 poll() poll(time,unit) take() remove()
讀取 peek() element()

二、源碼

  • ArrayBlockingQueue

    下面以ArrayBlockingQueue爲例看看JDK的源碼,其他的實現類,有先看一下它的主要屬性:
        public class ArrayBlockingQueue<E> extends AbstractQueue<E>  
                implements BlockingQueue<E>, java.io.Serializable{  
            /** 底層用於存放隊列元素的數組 */  
            final Object[] items;  
          
            /** 下一個獲取索引,take,poll,peek和remove會用到 */  
            int takeIndex;  
          
            /** 下一個插入索引,put,offer和add方法會用到 */  
            int putIndex;  
          
            /** 當前隊列中元素的個數 */  
            int count;  
          
            /** 用於控制併發操作隊列的鎖對象 */  
            final ReentrantLock lock;  
            /** 隊列爲非空的條件對象,用於喚醒阻塞中的讀操作 */  
            private final Condition notEmpty;  
            /** 隊列爲非滿的條件對象,用於喚醒阻塞中的寫操作 */  
            private final Condition notFull;  
        }  
  • 寫入

    1. offer(e)與offer(e,time,unit)
    首先是offer(e),邏輯比較簡單,上鎖、判斷插入(不滿則插入,滿了則返回)、解鎖。
        public boolean offer(E e) {  
            if (e == null) throw new NullPointerException();  
            final ReentrantLock lock = this.lock;  
            //鎖住隊列,防止在插入過程中的併發讀寫  
            lock.lock();  
            try {  
                //滿了,返回false  
                if (count == items.length)  
                    return false;  
                else {  
                    //執行插入  
                    insert(e);  
                    return true;  
                }  
            } finally {  
                //釋放鎖  
                lock.unlock();  
            }  
        }  

    而offer(e,time,unit)相比offer(e)則多了阻塞給定時間的功能,注意下面的無條件for循環是爲了防止多個線程同時被喚醒操作隊列,因此每次都需要判斷隊列是否已滿,是的話繼續阻塞:
        public boolean offer(E e, long timeout, TimeUnit unit)  
                throws InterruptedException {  
          
            if (e == null) throw new NullPointerException();  
                long nanos = unit.toNanos(timeout);  
            //使用lockInterruptibly鎖住隊列,可以被中斷  
            final ReentrantLock lock = this.lock;  
            lock.lockInterruptibly();  
            try {  
                for (;;) {  
                    //如果隊列不滿,則執行插入並返回true  
                    if (count != items.length) {  
                        insert(e);  
                        return true;  
                    }  
          
                    //如果nanos<=0,說明已經阻塞超過了給定時間了,直接返回false  
                    if (nanos <= 0)  
                        return false;  
                    try {  
                        //若該條件沒被喚醒或者該線程沒被中斷,等待給定時間  
                        //如果等待中被喚醒,返回剩餘的等待時間  
                        nanos = notFull.awaitNanos(nanos);  
                    } catch (InterruptedException ie) {  
                        notFull.signal(); // propagate to non-interrupted thread  
                        throw ie;  
                    }  
                }  
            } finally {  
                //釋放鎖  
                lock.unlock();  
            }  
        }  

    看到兩個方法都是調用的insert(e)執行實際的插入,insert方法也比較簡單,插入、非空喚醒
        private void insert(E x) {  
            items[putIndex] = x;  
            putIndex = inc(putIndex);  
            ++count;  
            notEmpty.signal();  
        }  

    2.add(e)
    add方法其實是調用了父類AbstractQueue的add方法:
        public boolean add(E e) {  
            if (offer(e))  
                return true;  
            else  
                throw new IllegalStateException("Queue full");  
        }  
    很簡單,其實就是通過offer判斷當前隊列是否滿了,是就立刻拋出異常。

    3. put(e) 其實put就相當與無限阻塞的offer(e,time,unit),也是在無限循環裏面判斷隊列是否已滿並插入,否則阻塞:
        public void put(E e) throws InterruptedException {  
            if (e == null) throw new NullPointerException();  
            final E[] items = this.items;  
            final ReentrantLock lock = this.lock;  
            lock.lockInterruptibly();  
            try {  
                try {  
                    //每次被喚醒都判斷一下隊列是否滿了,是則繼續阻塞  
                    while (count == items.length)  
                        notFull.await();  
                } catch (InterruptedException ie) {  
                    notFull.signal(); // propagate to non-interrupted thread  
                    throw ie;  
                }  
                insert(e);  
            } finally {  
                lock.unlock();  
            }  
        }  
  • 移除

    讀取操作和寫入其實大同小異,底層和核心邏輯都差不多,如果理解了上面關於讀取的幾個方法的話,讀取就無需過多解釋了。
    1.poll()與poll(time,unit)
        public E poll() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                if (count == 0)
                    return null;
                E x = extract();
                return x;
            } finally {
                lock.unlock();
            }
        }
    
        public E poll(long timeout, TimeUnit unit) throws InterruptedException {
            long nanos = unit.toNanos(timeout);
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                //無限循環,確保每次喚醒都要判斷當前是否爲空
                for (; ; ) {
                    if (count != 0) {
                        E x = extract();
                        return x;
                    }
                    if (nanos <= 0)
                        return null;
                    try {
                        nanos = notEmpty.awaitNanos(nanos);
                    } catch (InterruptedException ie) {
                        notEmpty.signal(); // propagate to non-interrupted thread
                        throw ie;
                    }
    
                }
            } finally {
                lock.unlock();
            }
        }
    
        private E extract() {
            //讀取隊列頭元素並喚醒都有等待非滿線程
            final E[] items = this.items;
            E x = items[takeIndex];
            items[takeIndex] = null;
            takeIndex = inc(takeIndex);
            --count;
            notFull.signal();
            return x;
        }

    2.take()
        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                try {
                    //爲空的情況下,無限阻塞
                    while (count == 0)
                        notEmpty.await();
                } catch (InterruptedException ie) {
                    notEmpty.signal(); // propagate to non-interrupted thread
                    throw ie;
                }
                E x = extract();
                return x;
            } finally {
                lock.unlock();
            }
        }

    3.remove()
        public boolean remove(Object o) {
            if (o == null) return false;
            final E[] items = this.items;
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                //遍歷隊列中的元素,使用equals方法判定相等則移除
                int i = takeIndex;
                int k = 0;
                for (;;) {
                    if (k++ >= count)
                        return false;
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    i = inc(i);
                }
    
            } finally {
                lock.unlock();
            }
        }
  • 讀取

    首先說明一下,這下面的讀取方法都是指讀取隊列頭部的元素,因爲如果構建消息隊列,都是尾部插入,頭部讀取。讀取的兩個方法的核心邏輯其實都在peek()裏:上鎖 - 讀取 - 解鎖:
        public E element() {
            E x = peek();
            if (x != null)
                return x;
            else
                throw new NoSuchElementException();
        }
    
        public E peek() {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                //takeIndex會一直指向隊列的頭
                return (count == 0) ? null : items[takeIndex];
            } finally {
                lock.unlock();
            }
        }
以上就是我對ArrayBlockingQueue的理解,水平有限,懇請指教。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章