一篇搞定ArrayBlockingQueue,你信不信?

ArrayBlockingQueue

ArrayBlockingQueue 是基于数组的阻塞队列。

关于阻塞队列,我们首先应该阅读BlockingQueue接口,在这个接口中可以大致了解阻塞队列都有哪些功能。能更加容易的理解阻塞队列的作用。

继承关系

在这里插入图片描述

  • Iterable保证有迭代的功能
  • Collection,AbstractCollection说明ArrayBlockQueue也是具有普通集合的功能
  • Queue,AbstractQueue接口更近一步标识它是一个队列,且具有队列的功能
  • BlockingQueue表明它是一个阻塞队列

源码解析

本篇源码解析,只分享最核心的部分:主要属性,初始化,新增,取出,删除。

主要属性

解释都在代码块的注释里面

// 重要属性
/**
 * 队列存放在 object 的数组里面
 * 数组大小必须在初始化的时候手动设置,没有默认大小
 */
final Object[] items;

/**
 * 下次拿数据的时候的索引位置
 */
int takeIndex;

/**
 * 下次放数据的索引位置
 */
int putIndex;

/**
 * 队列中数据的个数
 */
int count;


//并发控制属性

/**
 * 可重入锁,控制并发插入和获取
 */
final ReentrantLock lock;

/**
 * take队列,只有在阻塞队列不是空的时候,才可以进行take,否则take阻塞
 */
private final Condition notEmpty;

/**
 * put队列,只有在阻塞队列不满的时候,才可以进行put,否则put阻塞
 */
private final Condition notFull;

初始化方式

ArrayBlockQueue的初始化必须指定容量

// 初始化

/**
 * 初始化方法,基于数组的阻塞队列的初始化方法没有无参的,只有有参构造。
 * 且一开始初始化必须指定大小。没有默认大小。
 */
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

/**
 * 初始化方法
 *
 * @param capacity 初始化必须指定容量。没有默认大小
 * @param fair     公平还是非公平,内部ReentrantLock使用
 */
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     公平还是非公平,内部ReentrantLock使用
 * @param c        初始化元素值,元素必须少于数组容量,否则抛异常
 */
public ArrayBlockingQueue(int capacity, boolean fair,
                          Collection<? extends E> c) {
    this(capacity, fair);

    final ReentrantLock lock = this.lock;
    lock.lock(); // Lock only for visibility, not mutual exclusion
    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();
    }
}

核心操作方法

新增

  • 数据新增都会按照 putIndex 的位置进行新增
/**
 * 队列新增数据
 * 新增,如果队列满,无限阻塞
 *
 * @param e 新增的元素
 * @throws InterruptedException
 */
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);
    } finally {
        // 释放锁
        lock.unlock();
    }
}

/**
 * 元素入队核心操作
 *
 * @param x 元素
 */
private void enqueue(E x) {
    final Object[] items = this.items;
    // putIndex表示本次插入的位置
    items[putIndex] = x;
    // 插入完之后要比较一下是否到队尾了,如果已经在队尾了,那么下一次新增
    // 新增的位置为队列头,也就是数组下标为0的位置
    if (++putIndex == items.length)
        putIndex = 0;
    // 队列元素个数 +1
    count++;
    // 唤醒一个因为队列空导致的等待线程
    notEmpty.signal();
}

从源码中,可以看出,其实新增就两种情况:

  • 本次新增的位置居中,直接新增,下图演示的是 putIndex 在数组下标为 5 的位置,还不到队尾,那么可以直接新增,计算下次新增的位置应该是 6;

在这里插入图片描述

  • 新增的位置到队尾了,那么下次新增时就要从头开始了,示意图如下:

在这里插入图片描述

上面这张图演示的就是这行代码:

if (++putIndex == items.length) putIndex = 0;

可以看到当新增到队尾时,下次新增会重新从队头重新开始。

取数据

  • 拿数据都是从队头开始拿数据
/**
 * 拿数据操作
 * 从队头开始拿数据
 *
 * @return
 * @throws InterruptedException
 */
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 如果队列为空,无限等待
        // 直到队列中有数据被 put 后,自己被唤醒
        while (count == 0)
            notEmpty.await();
        // 唤醒之后,进行出队操作
        return dequeue();
    } finally {
        // 释放锁
        lock.unlock();
    }
}

/**
 * 出队操作
 */
private E dequeue() {

    final Object[] items = this.items;
    // 直接获取下一个可以出队的位置的元素
    E x = (E) items[takeIndex];
    // 将出了队的元素位置的引用置为null,帮助GC进行垃圾回收
    items[takeIndex] = null;
    // 计算下一次出队的位置,如果已经到了队尾,那么下一个出队的就是下标为0的元素
    if (++takeIndex == items.length)
        takeIndex = 0;
    // 队列元素个数 -1
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    // 唤醒被队列满所阻塞的线程
    notFull.signal();
    return x;
}

从源码中可以看出,每次拿数据的位置就是 takeIndex 的位置,在找到本次该拿的数据之后,会把 takeIndex 加 1,计算下次拿数据时的索引位置,有个特殊情况是,如果本次拿数据的位置已经是队尾了,那么下次拿数据的位置就要从头开始,就是从 0 开始了。

删除数据

删除数据有两种情况:

  • 删除位置和 takeIndex 的关系:删除位置和 takeIndex 一样,比如 takeIndex 是 2, 而要删除的位置正好也是 2,那么就把位置 2 的数据置为 null ,并重新计算 takeIndex 为 3。
  • 找到要删除元素的下一个,计算删除元素和 putIndex 的关系
    • 如果下一个元素不是 putIndex,就把下一个元素往前移动一位
    • 如果下一个元素是 putIndex,把 putIndex 的值修改成删除的位置
/**
 * 删除
 */
void removeAt(final int removeIndex) {

    final Object[] items = this.items;
    // 删除位置和 takeIndex 一样,也就是要删除的元素和要取出的元素是一样的。
    // 这种情况下,现将要删除的元素删除掉,然后重新计算一个takeIndex(取出的位置)
    if (removeIndex == takeIndex) {
        // 要删除的元素置为null
        items[takeIndex] = null;
        // 计算下一个可以取出元素的位置
        if (++takeIndex == items.length)
            takeIndex = 0;
        // 元素个数 -1
        count--;
        if (itrs != null)
            itrs.elementDequeued();
    } else {
        // 否则要看待删除的元素位置和待插入的元素的位置关系
        // 如果下一个元素不是 putIndex,就把下一个元素往前移动一位
        // 如果下一个元素是 putIndex,把 putIndex 的值修改成删除的位置
        final int putIndex = this.putIndex;
        for (int i = removeIndex; ; ) {
            int next = i + 1;
            if (next == items.length)
                next = 0;
            // 如果下一个元素不是 putIndex,就把下一个元素往前移动一位
            if (next != putIndex) {
                items[i] = items[next];
                i = next;
            } else {
                //如果下一个元素是 putIndex,把 putIndex 的值修改成删除的位置
                items[i] = null;
                this.putIndex = i;
                break;
            }
        }
        count--;
        if (itrs != null)
            itrs.removedAt(removeIndex);
    }
    notFull.signal();
}
图示
  • 第一种情况是 takeIndex == removeIndex

在这里插入图片描述

  • 第二种情况分为两种

    • 如果 removeIndex + 1 != putIndex 的话,就把下一个元素往前移动一位,示意图如下
      在这里插入图片描述

    • 如果 removeIndex + 1 == putIndex 的话,就把 putIndex 的值修改成删除的位置,示意图如下:

在这里插入图片描述

总结

ArrayBlockQueue就先总结这么点知识。回顾它的几个重点:

  • 有界数组作为底层数据结构
  • 当 takeIndex、putIndex 到队尾的时候,都会重新从 0 开始循环。(相当于是一个环)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值