JUC 之队列 ArrayBlockingQueue 详解

JUC 之队列 ArrayBlockingQueue 详解

通过类名我们就可以知道是通过数组实现的 BlockingQueue 阻塞队列。既然已经猜到了底层实现,那么我就开始进入其源码一探究竟,看看它是如何通过数组实现的阻塞队列。首先我们先来看看怎么用,这里给一个简单示例:

public static void main(String[] args) throws Exception {
    ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<>(2);
    abq.add(1);
    abq.offer(2);
    abq.put(3);// put 方法在队列满时会阻塞
    Integer take = abq.take();// take 方法在队列空时阻塞
    Integer poll = abq.poll();
}

ArrayBlockingQueue 使用方式很简单,后面我要重点关注的是它是如何实现 put 与 take 的阻塞操作的。

核心成员介绍

// 由于不知道具体要存放的数据类型,所以此处声明为 Object 类型的数组
final Object[] items;
// 指向下一个要出队的元素索引下标
int takeIndex;
// 指向下一个存放元素的索引下标
int putIndex;
// 记录数组中的元素个数
int count;
// 控制多线程并发操作的安全性
final ReentrantLock lock;
// 数组空时阻塞获取线程的条件变量
private final Condition notEmpty;
// 数组满时阻塞添加线程的条件变量
private final Condition notFull;
// 在构造器中初始化数组项,锁信息,条件信息
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();
}

核心方法讲解

offer 方法

分两种实现,一种不带超时等待的添加操作,一种带有超时等待的添加操作,具体源码如下,而 add 方法最终也是调用该方法完成最终的添加操作。

public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();// 上锁,此处可以直接使用 this.lock.lock() 方式上锁
    try {
        if (count == items.length)// 校验数组时候已满
            return false;
        else {
            enqueue(e);// 实际添加操作
            return true;
        }
    } finally {
        lock.unlock();
    }
}

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);// 调用 Condition 的 awaitNanos 方法阻塞等待指定时间
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}
enqueue 方法

元素添加队列操作,通过 putIndex 索引完成操作。添加成功后调用 notEmpty.signal() 唤醒阻塞的 take 获取数据的线程,唤醒流程可参考【AQS 之 Condition 源码剖析】一篇的讲解。

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;// 将数据放入 putIndex 索引下标处
    if (++putIndex == items.length)// 数组满了,将 putIndex 置 0
        putIndex = 0;
    count++;// 计数加 1
    notEmpty.signal();// 唤醒阻塞线程
}
put 方法

通过 count 计数器校验数组是否已满,满了调用 notFull.await() 方法进行阻塞,否则调用 enqueue 方法将数据添加到队列中,阻塞流程可参考【AQS 之 Condition 源码剖析】一篇的讲解。由于 put 方法会响应中断,所以在获取锁时调用的是 ReentrantLock 的 lockInterruptibly方法,该方法的实现可参考【JUC 之 ReentrantLock 源码解析】一篇的中的 lock 方法的讲解,两个方法的区别就在于 lock 方法不响应中断,而 lockInterruptibly 方法响应中断,其他实现都一样。这两个方法最终会调用 AQS 中 acquire 方法和 acquireInterruptibly 方法

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); // 调用 enqueue 方法将数据添加到队列中
    } finally {
        lock.unlock();
    }
}
poll 方法

从队列中获取数据,通过 takeIndex 索引实现获取操作。

public E poll() {// 队列为空时直接返回 null 
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();// 不为空时 调用 dequeue 方法完成获取操作
    } finally {
        lock.unlock();
    }
}
// 队列为空时阻塞等待指定时间,还是没有数据返回 null,阻塞阶段可被中断唤醒
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);// 调用 Condition 的 awaitNanos 方法阻塞等待指定时间
        }
        return dequeue();// 不为空时 调用 dequeue 方法完成获取操作
    } finally {
        lock.unlock();
    }
}

dequeue 方法

实际获取操作,通过 takeIndex 从数组中取出该索引下标处的元素并从队列中删除该数据,取出数据后唤醒阻塞在 notFull 条件上的添加数据的线程(也即生产数据的线程)。

private E dequeue() {
    final Object[] items = this.items;
    E x = (E) items[takeIndex];
    items[takeIndex] = null;// 获取到数据后,清空数组中该下标处的值
    if (++takeIndex == items.length)
        takeIndex = 0; // 队列中没有数据了
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();// 唤醒操作
    return x;
}
peek 方法

通过 takeIndex 从数组中取出该索引下标处的元素,不会从队列中删除该数据,这是与 poll 方法不同的地方。当队列为空时,peek() 方法将返回 null

public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return itemAt(takeIndex); // null when queue is empty
    } finally {
        lock.unlock();
    }
}
final E itemAt(int i) {
    return (E) items[i];
}
take 方法

队列为空时,调用 Condition 的 await 方法进行阻塞等待,否则调用 dequeue 从队列中获取数据,该方法可响应中断

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();// 队列中没有数据,阻塞等待
        return dequeue();// 获取数据
    } finally {
        lock.unlock();
    }
}
remove 方法

从队列中删除指定元素。从 takeIndex 处遍历比较数据是否是要删除的数据,然后调用 removeAt 方法完成删除操作

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

void removeAt(final int removeIndex) {
    final Object[] items = this.items;
    if (removeIndex == takeIndex) {// 要删除的数据就存放在 takeIndex 处,直接删除即可,将 takeIndex 索引处置空
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
    } else {// 要删除的数据不在 takeIndex 处
        final int putIndex = this.putIndex;
        for (int i = removeIndex;;) {// 从 removeIndex 处开始循环遍历将其后面的数据向前移动,然后将最后putIndex处的数据置空(因为 putIndex 中的数据已经向前移动到了前一个索引下标处了,也即 putIndex - 1 处),最后更新 putIndex 索引值
            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();
}

总结:上述就是我们在使用 ArrayBlockingQueue 时的常用方法了,通过上述学习我们很容易发现,通过 ArrayBlockingQueue 我们可以很方便的就能实现一个生产者消费者模型。通过一个 ReentrantLock 和两个 Condition 条件变量 notEmpty(队列空时阻塞消费者线程)、notFull(当队列满了阻塞生产者线程)来实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值