【Java 并发】【十】【JUC数据结构】【四】ArrayBlockingQueue阻塞队列原理

1  前言

这节我们就来看看ArrayBlockingQueue内部实现的原理。ArrayBlockingQueue阻塞队列是基于数组来实现的,上一章节的LinkedBlockingQueue是基于链表来实现的。ArrayBlockingQueue内部的实现机制跟LinkedBlockingQueue是几乎一样的。这节我们就简单来过一下ArrayBlockingQueue这个阻塞队列。

2  ArrayBlockingQueue内部源码

2.1  内部属性

我们先来看一下ArrayBlockingQueue内部有哪些属性:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    private static final long serialVersionUID = -817911632652898426L;

    // 队列数组,这里使用数组来存储数据
    // 提示:ArrayBlockingQueue使用环形数组来存储数据和取出数据
    final Object[] items;
    // 这里是当前取到数组的第几个位置,初始化是0
    int takeIndex;
    // 这里是插入到数组的第几个位置,初始化是0
    int putIndex;
    // 当前阻塞队列的大小,具有多少个元素
    int count;
    // 锁,注意这里与LinkedBlockingQueue不同
    // 这里只有一把锁,插入的时候和拉取数据的时候都是使用这同一把锁
    final ReentrantLock lock;
    // 取数据的等待条件,队列非空
    private final Condition notEmpty;
    // 插入数据的等待条件,队列未满
    private final Condition notFull;
}

2.2  构造方法

public ArrayBlockingQueue(int capacity, boolean fair) {
    // 必须要指定数组的长度,并且capacity容量必须大于0
    if (capacity <= 0)
        throw new IllegalArgumentException();
    // 这里的capacity要做数组长度初始化,所以是必须的
    this.items = new Object[capacity];
    // 这里fair代表的是使用的是公平锁还是非公平锁
    lock = new ReentrantLock(fair);
    // 去数据的条件,非空
    notEmpty = lock.newCondition();
    // 插入数据的条件,容量未满
    notFull = lock.newCondition();
}
public ArrayBlockingQueue(int capacity) {
    // 这里必须传一个capacity容量大小,默认使用的是非公平锁
    this(capacity, false);
}

根据上面讲解的ArrayBlockingQueue的属性,我们还是画个图理解一下:

(1)首先队列使用数组来存储元素,有一个items的Object数组
(2)有两个指针构成环形数组,分别是putIndex插入位置指针,takeIndex获取数据指针
(3)有一个int 类型的count属性,表示当前队列的大小;items数组的长度length就是当前队列的最大容量
(4)有一把锁lock,插入队列和从队列获取数据的时候都需要对同一把锁进行上锁,注意,这里是与LinkedBlockingQueue不一样的地方,LinkedBlockingQueue是两把锁,插入一把锁,获取一把锁;而这里是共用同一把锁
(5)两个Condition等待条件,notFull表示插入条件,容量未满的时候可以插入;notEmpty表示取数条件,队列不是空的时候可以取数据

2.3  put方法

我们来看一下put方法的源码流程:

public void put(E e) throws InterruptedException {
    // 首先检查下插入元素是否是空,如果是空则抛出异常
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    // 进行插入之前要进行加锁
    lock.lockInterruptibly();
    try {
        // 这里判断容量是否满了
        while (count == items.length)
            // 如果对接的容量满了,说明notFull条件不满足
            // 此时插入线程调用notFull.await方法进入等待队列阻塞等待
            notFull.await();
        // 走到这里,说明队列未满,插入一个元素。
        // 并且插入成功之后,队列有元素了,唤醒一下取数而被阻塞的线程,让它们醒来取数据
        enqueue(e);
    } finally {
        // 这里就是释放锁了,常规操作
        lock.unlock();
    }
}

2.3.1  enqueue方法

private void enqueue(E x) {
    // 获取存储数据的数组
    final Object[] items = this.items;
    // 这里就是往putIndex的数组位置插入一个元素
    items[putIndex] = x;
    // 同时插入位置右移一个位置
    // 如果移动到了数组尽头了,此时重新回到数组的下标为0的位置
    // 这里的数组就是环形数组,如果不懂环形数组的童鞋,需要上网了解下哈
    if (++putIndex == items.length)
        putIndex = 0;
    // 数组元素的个数+1
    count++;
    // 插入成功,此时阻塞队列肯定不为空
    // 调用notEmpty.singal方法唤醒一下之前可能取数据是空而阻塞等待的线程
    notEmpty.signal();
}

上面就是put方法的大致逻辑,我们画个图理解一下:

(1)线程A调用put方法插入数据,首先需要调用lock.lock方法进行加锁
(2)加锁成功之后判断当前队列是否满了,也就是count == items.length是否成立
(3)如果满了,则调用notFull.await方法去等待队列阻塞等待,等待别人取数据后,队列有空余位置插入的时候将它唤醒
(4)如果队列未满,那就简单了,往数组的putIndex位置放入插入的元素
(5)插入成功之后,此时队列有数据了,调用notEmpty.singal方法唤醒一个取数线程,告诉它有数据可以取了

2.4  take方法

我们继续看获取take的源码:

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    // 取数前进行加锁
    lock.lockInterruptibly();
    try {
        // 判断当前队列是否是空的
        while (count == 0)
            // 如果队列是空的,notEmpty非空条件不满足
            // 此时调用await方法进入等待队列阻塞等待
            notEmpty.await();
        // 走到这里,说明队列非空,此时取出一个数据
        // 并且取出一个数据后,队列空余了一个位置,唤醒正在插入阻塞的线程,有空余位置了,可以插入数据了
        return dequeue();
    } finally {
        // 常规操作,这里就是释放锁了
        lock.unlock();
    }
}

这里我们结合put、take画一个整体的图,来理解一下:

(1)线程B调用take方法获取数据,首先要进行加锁调用lock.lockInterruptibly()方法
(2)加锁成功之后,判断当前队列是否为空,也就是count == 0是否成立
(3)如果队列是空的,没有数据可以获取,此时取数线程就需要调用notEmpty.await方法进入等待队列阻塞等待
(4)如果队列非空,此时就获取一个元素,队列多空出了一个位置,队列肯定是未满的,此时就可以调用notFull.singal方法唤醒一个插入阻塞的线程,告诉它有空余的位置可以插入数据了

ArrayBlockingQueue的其它方法这里就不看了哈,原理都是大同小异的,跟LinkedBlockingQueue实现的原理都差不多。

3  小结

好了,ArrayBlockingQueue就到这里了,可以跟LinkedBlockingQueue对照着看,主要是用的数据结构不一样以及锁的个数不一样哈,有理解不对的地方欢迎指正哈。

posted @ 2023-04-09 18:16  酷酷-  阅读(57)  评论(0编辑  收藏  举报