ArrayBlockingQueue源码解析

阅读须知

  • JDK版本:1.8
  • 文章中使用/* */注释的方法会做深入分析

正文

ArrayBlockingQueue,命名上就能看出其含义,基于数组的 FIFO 阻塞队列,由 Doug Lea 大神开发,在 jdk1.5 版本跟我们见面,关于阻塞队列的应用这里不多赘述,相信大家都有所了解,我们直接来看源码实现。首先我们来看内部的成员变量:
ArrayBlockingQueue:

// 用于存储队列内部的元素
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;
// 当前活动迭代器的共享状态,如果没有,则为 null,允许队列操作更新迭代器状态
transient Itrs itrs = null;

下面我们来看一下构造方法:
ArrayBlockingQueue:

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    // 初始化队列数组,capacity 为容量
    this.items = new Object[capacity];
    // fair 为公平或非公平锁的标记
    lock = new ReentrantLock(fair);
    // 初始化入队和出队操作的等待条件
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

下面我们来看入队和出队相关的几个方法的实现,首先我们来看入队操作:
ArrayBlockingQueue:

public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count == items.length)
        	// 如果元素的数量已经和数组的长度相等,证明队列已满,返回 false 代表入队失败
            return false;
        else {
        	/* 入队操作 */
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

ArrayBlockingQueue:

private void enqueue(E x) {
    final Object[] items = this.items;
    // 将元素放在数组的 putIndex 下标对应的位置
    items[putIndex] = x;
    if (++putIndex == items.length)
    	// 如果 putIndex 自增后与数组的长度相等,说明这次入队操作的元素已经放到数组的最后一个位置,所以重置 putIndex 为0,下一个元素入队后从下标为0开始存储
        putIndex = 0;
    // 元素成功入队后将当前队列中元素的数量自增1
    count++;
    // 唤醒等待非空条件的线程,表示当前队列中有元素了,可以获取了
    notEmpty.signal();
}

不知道大家是否有疑问,这里如果将 putIndex 重置为0之后,下一个入队的元素就会放在0这个位置,如果这个位置的元素还没有被取出,那不就覆盖了么?其实并不会发生覆盖,ArrayBlockingQueue 为 FIFO 队列,所以元素会按顺序进行入队和出队,在 offer 方法的开始校验了count == items.length,所以如果0下标的元素没有被取出,这个判断就会为 true 代表数组已经满了,这时 offer 方法返回 false 代表插入失败,所以并不会有覆盖的情况发生,这同时也说明了 offer 方法是不会阻塞的。

还有两个入队的方法 add 和 put,add 方法最终调用了 offer 方法,所以流程是一样的,我们来看另一个入队方法 put:
ArrayBlockingQueue:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
        	// 如果元素的数量已经和数组的长度相等,证明队列已满,将线程阻塞在 notFull 条件上等待唤醒
            notFull.await();
        // 入队操作
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

put 方法和 offer 方法唯一的区别就是在于对数组元素已满情况下的处理,offer 方法会直接返回 false 代表失败,而 put 方法会将当前线程阻塞在 notFull 条件上等待唤醒,所以我们猜测,在出队操作中一定有对等待在 notFull 条件上的线程的唤醒操作,我们来看出队方法的实现:
ArrayBlockingQueue:

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        /* 如果数组中的元素为空,返回 null,不为空做出队操作 */
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}

ArrayBlockingQueue:

private E dequeue() {
    final Object[] items = this.items;
    // 取出 takeIndex 对应的元素
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    // 将 takeIndex 位置的元素置为 null
    items[takeIndex] = null;
    if (++takeIndex == items.length)
    	// 如果出队索引自增后与数组的长度相等,说明本次出队操作已经取到了队尾,将 takeIndex 重置为0,下次出队操作从0开始取
        takeIndex = 0;
    // 出队成功,减少数组中元素的数量
    count--;
    if (itrs != null)
    	// 每当一个元素出队时调用
        itrs.elementDequeued();
    // 唤醒在 notFull 条件上等待的线程
    notFull.signal();
    return x;
}

下面我们来看另外几个出队方法,peek 方法直接获取数组 takeIndex 下标的元素,不会将下标位置的元素置为 null,我来看 take 方法:
ArrayBlockingQueue:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
        	// 如果元素的数量为0,证明队列已空,将线程阻塞在 notEmpty 条件上等待唤醒
            notEmpty.await();
        // 出队操作
        return dequeue();
    } finally {
        lock.unlock();
    }
}

同样的,与 poll 方法的区别就是在于数组元素为空时的处理,poll 方法会返回 null,take 方法会阻塞。poll 方法还有一个重载的实现,这个方法可以指定等待的时间:
ArrayBlockingQueue:

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
	// 转换时间单位
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 同样的数组元素个数为0的判断
        while (count == 0) {
            if (nanos <= 0)
                return null;
            // 设置超时时间的阻塞
            nanos = notEmpty.awaitNanos(nanos);
        }
        // 出队操作
        return dequeue();
    } finally {
        lock.unlock();
    }
}

ArrayBlockingQueue:

public boolean remove(Object o) {
    if (o == null) return false;
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	// 数组元素数量大于0移除操作才有意义
        if (count > 0) {
            final int putIndex = this.putIndex;
            // 从 takeIndex 开始遍历
            int i = takeIndex;
            do {
                if (o.equals(items[i])) {
                	// equals 判断为 true,则移除对应位置的元素,返回成功
                    removeAt(i);
                    return true;
                }
                if (++i == items.length)
                	// 自增后与数组长度比较,如果相等,证明已经遍历到数组的最后一个下标,重置为0再次遍历
                    i = 0;
            // 如果待移除的索引值与 putIndex 相等,结束循环,说明没有找到匹配的元素
            } while (i != putIndex);
        }
        return false;
    } finally {
        lock.unlock();
    }
}

到这里,入队和出队操作就分析完了。我们在分析线程池源码的时候看到过,我们在使用 shutdownNow 方法停止线程池的时候,会调用 BlockingQueue 的 drainTo 方法移除此队列中所有可用的元素,并将它们添加到给定 collection 中,我们来看一下ArrayBlockingQueue对相关方法的实现:

public int drainTo(Collection<? super E> c, int maxElements) {
    checkNotNull(c);
    if (c == this)
        throw new IllegalArgumentException();
    // maxElement 是想要移除的元素的数量
    if (maxElements <= 0)
        return 0;
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 比较 maxElements(另一个重载的 drainTo 方法会将此值传为 Integer.MAX_VALUE)和元素的数量,取较小的一个
        int n = Math.min(maxElements, count);
        // 从 takeIndex 开始
        int take = takeIndex;
        int i = 0;
        try {
            // n - i 就是要操作的元素的个数
            while (i < n) {
                @SuppressWarnings("unchecked")
                E x = (E) items[take];
                // 将元素添加到给定集合中
                c.add(x);
                // 将数组的对应下标位置置为 null
                items[take] = null;
                if (++take == items.length)
                	// 如果已经遍历到数组的最后一个位置,重置为0
                    take = 0;
                i++;
            }
            return n;
        } finally {
            if (i > 0) {
            	// count - i 就是剩余元素的数量
                count -= i;
                // 将 takeIndex 置为当前取到的下标
                takeIndex = take;
                if (itrs != null) {
                    if (count == 0)
                        // 通知所有活动迭代器队列为空,清除所有弱引用,并且断开它的数据结构
                        itrs.queueIsEmpty();
                    else if (i > take)
                        // i > take 说明 take 发生了重置为0的情况,这时要通知所有迭代器,并删除所有旧的迭代器
                        itrs.takeIndexWrapped();
                }
                for (; i > 0 && lock.hasWaiters(notFull); i--)
                    notFull.signal();
            }
        }
    } finally {
        lock.unlock();
    }
}

到这里,ArrayBlockingQueue 的源码解析就完成了。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值