【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对照着看,主要是用的数据结构不一样以及锁的个数不一样哈,有理解不对的地方欢迎指正哈。