挖掘队列源码之ArrayBlockingQueue

前言

上一段时间一直在探索线程池的路上,如今准备踏入到队列,还是按照一开始的阅读方式。探索ArrayBlockingQueue是基于JDK1.8,从注释上可以得知是一个由数组支持的有界阻塞队列,按照先进先出的顺序,新元素被插入到队列的尾部,从队列头部获取元素,提供在将新元素放入到饱满的队列中会导致阻塞,直到队列出现新的位置才会被唤醒继续往下操作的方法,还支持在从空队列中获取元素会导致阻塞,直到队列出现元素才会被唤醒的方法。有一个点需要注意下,ArrayBlockingQueue在某些情况下插入新元素时会执行相当于循环的操作,怎么表述呢,当在数组的最后一个索引处上插入新元素后,那么插入下一个新元素的索引将会是0,有点类似循环,而由于LinkedBlockingQueue不同的数据结构并不会发生这种情况,还有一点就是ArrayBlockingQueue内部中只有一个锁,所以对于读写来说,同时只能有一个操作在执行,要么是读要么是写,而LinkedBlockingQueue采用的是两个锁的读写分离,好了,接下来直接进入主题吧。

数据结构


    public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {

        // 用于存储插入的元素
        final Object[] items;

        // 下一个获取的索引
        int takeIndex;

        // 下一个插入的索引
        int putIndex;

        // 队列中元素的个数
        int count;

        // 可重入锁,防止多线程并发访问队列
        final ReentrantLock lock;

        // 从空队列中获取元素导致阻塞直到新元素插入时被唤醒
        private final Condition notEmpty;

        // 新元素放入到饱满的队列中导致阻塞直到队列出现空位置时被唤醒
        private final Condition notFull;

        // 迭代器
        transient Itrs itrs = null;

    }

构造函数


    /**
     * 初始化
     * @param capacity 数组的大小
     */
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

    /**
     * 初始化
     * @param capacity 数组的大小
     * @param fair true:队列按照先进先出的顺序访问,false:不确定的顺序访问
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

    /**
     * 按顺序插入集合中的元素并初始化
     * @param capacity 数组的大小
     * @param fair true:队列按照先进先出的顺序访问,false:不确定的顺序访问
     * @param c 集合
     */
    public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

简单方法


    /**
     * 循环递减i
     * 若 i = 0  -> i = items.length - 1
     * 若 i > 0  -> i = i -1
     * @param i 变量
     * @return 结果值
     */
    final int dec(int i) {
        return ((i == 0) ? items.length : i) - 1;
    }

    /**
     * 获取指定索引处的元素
     * @param i 指定索引
     * @return 结果值 
     */
    final E itemAt(int i) {
        return (E) items[i];
    }

    /**
     * 插入元素
     * @param x 元素
     */
    private void enqueue(E x) {
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal(); // 已经插入元素了,应该唤醒等待获取元素的线程
    }

    /**
     * 出队列(获取元素)
     * @return 结果值
     */
    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;
    }

    /**
     * 移除指定索引处的元素
     * @param removeIndex
     */
    void removeAt(final int removeIndex) {
        final Object[] items = this.items;
        /**
         * 仔细观察可以发现 takeIndex ~ putIndex - 1 之间的元素都不为null,为了保证如此结构只有当removeIndex介于这两者之间时才需要将removeIndex后续的元素往前移动
         * 而其他情况如 removeIndex == takeIndex 或 removeIndex == putIndex - 1只要将其置为null即可
         * 但在else代码片段中不考虑其他调用代码的情况下还是有可能出现removeIndex < takeIndex 或 removeIndex > putIndex的情况,但这两种情况并不影响,实际上在调用代码中并未出现这两种情况
         * 所以实际上只判断了 takeIndex < removeIndex < putIndex 
         */
        if (removeIndex == takeIndex) { // 当removeIndex == takeIndex时直接将其置为null即可
            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;;) { // 在takeIndex < removeIndex < putIndex的情况下需要将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(); // 已经有元素出队列了,应该唤醒等待插入新元素的线程
    }

    /**
     * 插入元素
     * 实际上调用了offer方法,当队列饱满的情况下会抛出异常
     * @param e 元素
     * @return 是否插入成功
     */
    public boolean add(E e) {
        return super.add(e);
    }

    /**
     * 插入元素
     * 当队列饱满的情况下会返回false
     * @param e 元素
     * @return 结果值
     */
    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();
        }
    }

    /**
     * 插入元素
     * 当队列饱满的情况下会一直阻塞等待,直到被唤醒或被中断
     * @param e 元素
     * @return 结果值
     */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); // lock.lockInterruptibly与lock.lock的区别在于前者是可以发生中断的,也就是会直接抛出异常退出程序,而后者则不允许,两者都会锁住线程
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 插入元素
     * 当队列饱满的情况下会阻塞等待指定时间,直到被唤醒或被中断或超时
     * @param e 元素
     * @param timeout 指定时间
     * @param unit 时间单位
     * @return 结果值
     */
    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();
        }
    }

    /**
     * 获取元素
     * 当队列为空的情况下直接返回null
     * @return 结果值
     */
    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取元素
     * 当队列为空的情况下一直阻塞等待,直到被唤醒或被中断
     * @return 结果值
     */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取元素
     * 当队列为空的情况下阻塞等待指定时间,直到被唤醒或被中断或超时
     * @param timeout 指定时间
     * @param unit 时间单位
     * @return 结果值
     */
    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();
        }
    }

    /**
     * 获取元素
     * 与前面的三个方法较大不同,因为它调用的itemAt方法不会导致takeIndex索引自增,所以该方法即使调用多次依然能获取到元素,而其他三个方法则不行
     * @return 结果值
     */
    public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }

    /**
     * 移除元素
     * @param o 元素
     * @return 结果值 
     */
    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 { // 遍历查找 takeIndex ~ putIndex -1 之间的元素
                    if (o.equals(items[i])) {
                        removeAt(i);
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 清空队列
     */
    public void clear() {
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int k = count;
            if (k > 0) {
                final int putIndex = this.putIndex;
                int i = takeIndex;
                do { // 遍历队列并清空
                    items[i] = null;
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
                takeIndex = putIndex; // 细节部分,takeIndex = putIndex是为了下次插入新元素后可以获取到
                count = 0;
                if (itrs != null)
                    itrs.queueIsEmpty();
                for (; k > 0 && lock.hasWaiters(notFull); k--)
                    notFull.signal();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 将队列中的元素添加到集合中,添加指定个数
     * 这些被添加到集合的元素将会使对应队列上的位置为null,相当于从队列拿出去了
     * @param c 集合
     * @param maxElements 指定元素个数
     * @return 指定元素个数
     */
    public int drainTo(Collection<? super E> c, int maxElements) {
        checkNotNull(c);
        if (c == this)
            throw new IllegalArgumentException();
        if (maxElements <= 0)
            return 0;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int n = Math.min(maxElements, count);  // 将队列中的元素添加到集合的个数,最小值是maxElements,最大值是count
            int take = takeIndex;
            int i = 0;
            try {
                while (i < n) { // 从takeIndex开始遍历获取元素
                    @SuppressWarnings("unchecked")
                    E x = (E) items[take];
                    c.add(x); // 即使add发生异常,那么发生异常之前添加的元素依然存在
                    items[take] = null;
                    if (++take == items.length)
                        take = 0;
                    i++;
                }
                return n;
            } finally {
                // 队列中的元素被添加到集合后,该队列对应索引上的元素就会变成null,所以这里也要更新其索引与元素个数
                if (i > 0) {
                    count -= i;
                    takeIndex = take;
                    if (itrs != null) {
                        if (count == 0)
                            itrs.queueIsEmpty();
                        else if (i > take)
                            itrs.takeIndexWrapped();
                    }
                    for (; i > 0 && lock.hasWaiters(notFull); i--)
                        notFull.signal();
                }
            }
        } finally {
            lock.unlock();
        }
    }

    // 关于迭代器的内容就不做讲解了...

总结

ArrayBlockingQueue提供了多种插入/获取元素的方法,先简单说下区别:

  • 在队列饱满的情况下插入新元素

    • add:直接抛出异常。

    • offer:直接返回false。

    • put:一直阻塞等待直到被唤醒或线程被中断。

    • offer(time):阻塞等待指定时间,直到被唤醒或被中断或超时

  • 在队列为空的情况下获取元素

    • poll:直接返回null。

    • take:一直被阻塞等待直到被唤醒或线程被中断。

    • poll(time):阻塞等待直到被唤醒或线程被中断或超时

    • peek:直接返回null,该方法即使调用多次依然能获取到元素,而其余3个方法则不行。

关于ArrayBlockingQueue的知识点并不是很多,很容易掌握!

posted @ 2020-12-19 23:13  zliawk  阅读(68)  评论(0)    收藏  举报