BlockingQueue即阻塞队列,它算是一种将ReentrantLock用得非常精彩的一种表现,依据它的基本原理,我们可以实现Web中的长连接聊天功能,当然其最常用的还是用于实现生产者与消费者模式,大致如下图所示:
在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的
一、ArrayBlockingQueue
ArrayBlockingQueue底层是使用一个数组实现队列的,并且在构造ArrayBlockingQueue时需要指定容量,也就意味着底层数组一旦创建了,容量就不能改变了,因此ArrayBlockingQueue是一个容量限制的阻塞队列。因此,在队列全满时执行入队将会阻塞,在队列为空时出队同样将会阻塞。
1.1 参数及构造函数
BlockingQueue内部有一个ReentrantLock,其生成了两个Condition,在ArrayBlockingQueue的属性声明中可以看见:
// 存储队列元素的数组
final Object[] items;
// 拿数据的索引,用于take,poll,peek,remove方法
int takeIndex;
// 放数据的索引,用于put,offer,add方法
int putIndex;
// 元素个数
int count;
// 可重入锁
final ReentrantLock lock;
// notEmpty条件对象,由lock创建
private final Condition notEmpty;
// notFull条件对象,由lock创建
private final Condition notFull;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);//默认构造非公平锁的阻塞队列
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
//初始化ReentrantLock重入锁,出队入队拥有这同一个锁
lock = new ReentrantLock(fair);
//初始化非空等待队列
notEmpty = lock.newCondition();
//初始化非满等待队列
notFull = lock.newCondition();
}
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();
}
}
几个重要的点:
- 用到了ReentrantLock锁
- 用lock创建了两个Condition,notEmpty和notFull。
notEmpty是消费队列元素的等待队列(即take、peek、poll方法)
notFull是生产队列元素的等待队列(即put,offer,add方法) - 用的锁是非公平锁
- takeIndex和putIndex
1.2 put方法
put(E e)方法在队列不满的情况下,将会将元素添加到队列尾部,如果队列已满,将会阻塞,直到队列中有剩余空间可以插入。该方法的实现如下:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//当元素数量与数组的长度相等,无法添加元素
while (count == items.length)
//挂起当前线程,,添加到notFull条件队列中等待唤醒
notFull.await();
//添加元素到队列
enqueue(e);
} finally {
//释放锁
lock.unlock();
}
}
lock 与 lockInterruptibly比较区别在于:
lock 优先考虑获取锁,待获取锁成功后,才响应中断。
lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。
enqueue方法源码:
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//将元素放到putIndex位置
items[putIndex] = x;
/*如果putIndex+1后的值==数组的长度,则putIndex重置为0*/
if (++putIndex == items.length)
putIndex = 0;
count++;
//唤醒notEmpty
notEmpty.signal();
}
从enqueue方法可以看出ArrayBlockingQueue是查数据是按数组下标顺序插入数据的,当插入到最后一个下标时,会再从头开始插入元素。
关于put方法几个重要的点:
- 整个过程是被锁住的
- ArrayBlockingQueue不允许元素为null
- 当元素数量与数组的长度相等,无法添加元素,要唤醒notFull等待队列,让它从队列中取数据
- enqueue方法中,如果putIndex+1后的值==数组的长度,则putIndex重置为0
- 添加元素的最后一个步骤是唤醒notEmpty等待队列,即唤醒消费的等待队列线程
1.2.1 put方法总结与图解
put方法是一个阻塞的方法,如果队列元素已满,那么当前线程将会被notFull条件对象挂起加到等待队列中,直到队列有空档才会唤醒执行添加操作。但如果队列没有满,那么就直接调用enqueue(e)方法将元素加入到数组队列中。到此我们对三个添加方法即put,offer,add都分析完毕,其中offer,add在正常情况下都是无阻塞的添加,而put方法是阻塞添加。这就是阻塞队列的添加过程。说白了就是当队列满时通过条件对象Condtion来阻塞当前调用put方法的线程,直到线程又再次被唤醒执行。总得来说添加线程的执行存在以下两种情况,一是,队列已满,那么新到来的put线程将添加到notFull的条件队列中等待,二是,有移除线程执行移除操作,移除成功同时唤醒put线程,如下图所示
1.3 take方法
take()方法用于取走队头的元素,当队列为空时将会阻塞,直到队列中有元素可取走时将会被释放。其实现如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加锁
lock.lockInterruptibly();
try {
如果队列为空,唤醒notEmpty等待队列,让其插入元素
while (count == 0)
notEmpty.await();
//队列不为空,调用dequeue()出队
return dequeue();
} finally {
lock.unlock();
}
}
dequeue方法实现如下:
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
//拿到数组数据
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//拿到takeIndex下标的元素
E x = (E) items[takeIndex];
// 将数组中takeIndex索引位置设置为null
items[takeIndex] = null;
/*takeIndex索引加1并判断是否与数组长度相等,
如果相等说明已到尽头,恢复为0*/
if (++takeIndex == items.length)
takeIndex = 0;
//队列个数减1
count--;
if (itrs != null)
//同时更新迭代器中的元素数据
itrs.elementDequeued();
//删除了元素说明队列有空位,唤醒notFull条件对象添加线程,执行添加操作
notFull.signal();
return x;
}
关于take方法几个重要的点:
- 整个过程是被锁住的
- dequeue方法中,如果takeIndex+1后的值==数组的长度,则takeIndex重置为0。
与enqueue方法中的putIndex相似。保证了FIFO - 取出元素的最后一个步骤是唤醒notEmpty等待队列,即唤醒生产元素等待队列线程
1.3.1 take方法总结与图解
take方法其实很简单,有就删除没有就阻塞,注意这个阻塞是可以中断的,如果队列没有数据那么就加入notEmpty条件队列等待(有数据就直接取走,方法结束),如果有新的put线程添加了数据,那么put操作将会唤醒take线程,执行take操作。图示如下:
1.4 全流程图
1.5 ArrayBlockingQueue的方法对比
- add方法在添加元素的时候,若超出了度列的长度会直接抛出异常。
- put方法,若向队尾添加元素的时候发现队列已经满了会发生阻塞一直等待空间,以加入元素。
- offer方法在添加元素时,如果发现队列已满无法添加的话,会直接返回false。
- remove:若队列为空,抛出NoSuchElementException异常。
- take:若队列为空,发生阻塞,等待有元素。
- poll: 若队列为空,返回null。
- peek:该方法返回存在于此队列开头的元素
这些方法我就不做源码分析了,有兴趣可以自行查看,大致都差不多。
二、ArrayBlockingQueue与LinkedBlockingQueue异同
LinkedBlockingQueue和ArrayBlockingQueue的异同