详解ArrayBlockingQueue

168 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈


前言

ArrayBlockingQueue是基于数组实现的阻塞队列,本文将对ArrayBlockingQueueput()take() 方法的阻塞逻辑进行分析。

正文

ArrayBlockingQueue的类图如下所示。

ArrayBlockingQueue类图

ArrayBlockingQueue中持有一把重入锁lock,和一对ConditionnotEmptynotFull,这些都是在ArrayBlockingQueue的构造方法中创建出来的,如下所示。

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();
}

下面先分析ArrayBlockingQueueput() 方法,如下所示。

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();
    }
}

ArrayBlockingQueueput() 方法会先加锁,然后判断当前阻塞队列是否已满,如果已满则在notFull上进行等待(等待当前阻塞队列未满),否则调用enqueue() 方法将本次添加的元素入队列。分析enqueue() 方法前,先再看一下ArrayBlockingQueuetake() 方法的实现,如下所示。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 加锁并响应中断
    lock.lockInterruptibly();
    try {
        // 如果当前阻塞队列为空,则在notEmpty上进行等待
        while (count == 0)
            notEmpty.await();
        // 如果当前阻塞队列不为空,则将队列头元素出队列并返回
        return dequeue();
    } finally {
        lock.unlock();
    }
}

ArrayBlockingQueuetake() 方法也会先加锁,然后判断当前阻塞队列是否为空,为空则在notEmpty上进行等待(等待当前阻塞队列不为空),否则调用dequeue() 方法将队列头元素出队列并返回。

现在已知,当ArrayBlockingQueue已满时,调用put() 方法的线程会在notFull上进行等待,当ArrayBlockingQueue为空时,调用take() 方法的线程会在notEmpty上进行等待。那么现在假如ArrayBlockingQueue已满,线程中调用了put() 方法,此时线程阻塞在notFull上,相应的唤醒等待的操作就一定会出现在有元素出队列的时候,在take() 方法的最后调用了dequeue() 方法来将队列头元素出队列,下面看一下其实现。

private E dequeue() {
    final Object[] items = this.items;
    // 将items数组中takeIndex位置的元素获取出来
    // takeIndex位置的元素就是队列头元素
    E x = (E) items[takeIndex];
    // 置items数组中takeIndex位置为空
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        // 本次队列头元素在items数组最后一个位置
        // 则下一次队列头元素在items数组第一个位置
        takeIndex = 0;
    // 本次队列头元素出队列后,阻塞队列元素个数减1
    count--;
    // 因为发生了队列元素出队列,所以迭代器也要更新
    if (itrs != null)
        itrs.elementDequeued();
    // 因为发生了队列元素出队列,此时阻塞队列一定不满
    // 那么将在notFull上第一个等待的线程唤醒
    notFull.signal();
    return x;
}

首先dequeue() 方法中有一个叫做takeIndex的整型变量,该变量默认情况下初始值为0,作为元素数组items的下标索引使用,每次队列元素出队列时,均会让items数组中takeIndex索引位置的元素出队列,所以可以理解为takeIndex永远指向队列头。其次调用dequeue() 的线程一定会持有锁,并且队列头元素出队列后阻塞队列也一定不满,所以dequeue() 方法最后还会调用notFullsignal() 方法,唤醒第一个因为调用put() 方法时队列已满而在notFull上等待的线程。

现在假如ArrayBlockingQueue为空,线程中调用了take() 方法,此时线程阻塞在notEmpty上,相应的唤醒等待的操作就一定会出现在有元素入队列的时候,在put() 方法的最后调用了enqueue() 方法来将元素入队列,下面看一下其实现。

private void enqueue(E x) {
    final Object[] items = this.items;
    // 本次入队列的元素添加到items数组的putIndex位置
    items[putIndex] = x;
    if (++putIndex == items.length)
        // 本次入队列的元素添加到了items数组最后一个位置
        // 则下一次入队列的元素需要添加到items数组第一个位置
        putIndex = 0;
    // 本次元素入队列后,阻塞队列元素个数加1
    count++;
    // 因为发生了元素入队列,此时阻塞队列一定不为空
    // 那么将在notEmpty上第一个等待的线程唤醒
    notEmpty.signal();
}

dequeue() 方法相似,enqueue() 方法中使用了一个叫做putIndex的整型变量,该变量默认情况下初始值为0,作为元素数组items的下标索引使用,每次元素入队列时,入队列的元素会被添加到items数组的putIndex位置,所以可以理解为putIndex永远指向队列尾。此外,调用enqueue() 方法的线程也一定持有锁,并且元素入队列后阻塞队列一定不为空,所以enqueue() 方法最后还会调用notEmptysignal() 方法,唤醒第一个因为调用take() 方法时队列为空而在notEmpty上等待的线程。

总结

  1. ArrayBlockingQueue在创建时需要指定初始容量;
  2. ArrayBlockingQueue持有一把重入锁,ArrayBlockingQueue中的元素入队列的操作add()offer()put(),以及ArrayBlockingQueue中的元素出队列的操作remove()poll()take(),全都会进行加锁;
  3. ArrayBlockingQueue持有一对Condition,分别为notFullnotEmpty,当ArrayBlockingQueue已满且有线程调用put() 方法,此时调用put() 方法的线程会在notFull上等待,当ArrayBlockingQueue为空且有线程调用take() 方法,此时调用take() 方法的线程会在notEmpty上等待;
  4. 当有元素出队列后,会唤醒第一个在notFull上等待的线程,当有元素入队列后,会唤醒第一个在notEmpty上等待的线程;
  5. ArrayBlockingQueue使用一个Object数组items来存放元素,同时还有两个整型变量分别叫做takeIndexputIndex,默认情况下初始值均为0,takeIndex标识队列头元素在items数组中的位置,putIndex标识新入队列的元素应该被添加到items数组中的哪个位置,即takeIndex一直指向队列头,putIndex一直指向队列尾。

大家好,我是半夏之沫 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实最严谨的方式分享给大家,共同进步👉💓👈
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
👉👉👉👉👉👉👉👉💓关注微信公众号【技术探界】 💓👈👈👈👈👈👈👈👈

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情