这是一个依赖于数组实现的有界的阻塞队列,采用先进先出的规则,队列的头元素是入队最久的元素,队列的尾元素是入队时间最晚的元素。队列的大小在队列初始化的时候一旦确定了就没法进行改变,当尝试向满队列中插入数据和从空队列中获取数据都将产生阻塞。另一方面这个队列也可以保证等待线程的公平性,只需要再初始化的时候设置公平性即可,当然,默认情况下是非公平的。
ArrayBlockingQueue比较典型的使用就是用于实现生产者和消费者的模式,并且这里的队列大小的固定的。下面我们来看一下使用的示例:
import java.util.concurrent.ArrayBlockingQueue;
/**
* 演示ArrayBlockingQueue的使用
* @author: LIUTAO
* @Date: Created in 2018/9/27 19:36
* @Modified By:
*/
public class ArrayBlockingQueueDemo {
static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(5);
public static void main(String[] args) {
//producer线程
for (int i = 0; i < 10 ; i++){
new Thread(() -> {
try {
arrayBlockingQueue.put(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + "have put");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
//consumer线程
for (int i = 0; i < 10 ; i++){
new Thread(() -> {
try {
System.out.println(arrayBlockingQueue.take() + "have got");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行代码,我们可以发现生产者线程一开始就有五个线程执行完成,将数据加入了队列,而其余线程都是当消费者执行一下,然后生产者再执行。这是因为对列的容量为5,当队列被填满的时候生产生线程就只有等待。
这里我们来看一下类的继承关系。
从这里我们可以看出ArrayBlockingQueue拥有阻塞队列的特性。
//存放队列的数据
final Object[] items;
//即将被消费的数组数据索引
int takeIndex;
//即将存入的数组数据的索引
int putIndex;
//队列中的元素个数
int count;
//保证线程安全的重入锁
final ReentrantLock lock;
//不为空的条件
private final Condition notEmpty;
//有空闲位置的条件
private final Condition notFull;
从上面我们可以看出,为了保证线程的安全性和线程间的通信,这里使用了ReentrantLock和Condition。
针对构造函数,这里我们仅仅查看一个比较核心的就行。
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();
}
从上面我们可以看出构造函数其实就只做了两件事:(1)初始化数据存放数据。(2)初始化保证线程安全的重入锁。
核心函数我们通过查看“JUC--队列概述” 可以知道针对阻塞队列入队和出队一共有四组函数,这里我们仅仅分析put和take函数,其余的函数逻辑大同小异。
首先直接上put函数的源码:
public void put(E e) throws InterruptedException {
//检查e是否为空,为空则直接抛出NullPointerException
checkNotNull(e);
final ReentrantLock lock = this.lock;
//加锁
lock.lockInterruptibly();
try {
//当当前队列中的元素个数和数组大小相等的时候等待
while (count == items.length)
notFull.await();
//数据入队
enqueue(e);
} finally {
//释放锁
lock.unlock();
}
}
上面的逻辑比较清晰简单,我们就直接来看一下入队是怎么操作的。
private void enqueue(E x) {
//获得数据存储数组
final Object[] items = this.items;
//存入数据
items[putIndex] = x;
//当数据存入了数组的最后一个位置,为了保证FIFO规则,下一次从数组第一个位置开始存储
if (++putIndex == items.length)
putIndex = 0;
count++;
//唤醒处于等待状态的消费线程
notEmpty.signal();
}
上面的逻辑也比较简单,可谓充分使用了Condition,接下来我们来看一下take的实现。
同样,直接上源码:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//加锁,注意:这是响应中断的
lock.lockInterruptibly();
try {
//当当前队列没有数据的时候等待
while (count == 0)
notEmpty.await();
//出对
return dequeue();
} finally {
//释放锁
lock.unlock();
}
}
可以看出这里还是当没有数据的时候进行等待,知道put添加数据并调用notEmpty.signal()。
那么这里出对又是咋个实现的呢?
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;
}
总地说来,也比较简单,这里就不过多分析了。
经过上面的分心与学习,我们了解到,其实ArrayBlockingQueue就是使用数组存储数据,然后使用ReentrantLock来保证线程的安全性,使用Condition来进行线程间的通信。接下来博主会继续分析其他的队列类的实现,欢迎关注。