描述
ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个 数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你 可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实 现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
ArrayBlockingQueue 类图
如上图 ArrayBlockingQueue 内部有个数组 items 用来存放队列元素,putindex 下标标示入队元素下标, takeIndex 是出队下标,count 统计队列元素个数,从定义可知道并没有使用 volatile 修饰,这是因为访问这些变量使用都是在锁块内,并不存在可见性问题。另外有个独占锁 lock 用来对出入队操作加锁,这导致同时只有一个线程可以 访问入队出队,另外 notEmpty,notFull 条件变量用来进行出入队的同步。 另外构造函数必须传入队列大小参数,所以为有界队列,默认是 Lock 为非公平锁。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
/**
* capacity 数组长度
* fair 是否公平锁
*/
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();
}
注:
所谓公平锁:就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线 程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到 自己。
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
lock.newCondition 的作用可参考:http://www.importnew.com/30150.html
ArrayBlockingQueue 方法
offer 方法
在队尾插入元素,如果队列满则返回 false,否者入队返回 true。
public boolean offer(E e) {
//e 为 null,则抛出 NullPointerException 异常
checkNotNull(e);
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果队列满则返回 false
if (count == items.length)
return false;
else {
//否则插入元素
insert(e);
return true;
}
} finally {
//释放锁
lock.unlock();
}
}
private void insert(E x) {
//元素入队
items[putIndex] = x;
//计算下一个元素应该存放的下标
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
//循环队列,计算下标
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
这里由于在操作共享变量前加了锁,所以不存在内存不可见问题,加过锁后获取的共享变量都是从主内存获取的, 而不是在 CPU 缓存或者寄存器里面的值,释放锁后修改的共享变量值会刷新会主内存中。 另外这个队列是使用循环数组实现,所以计算下一个元素存放下标时候有些特殊。
另外 insert 后调用 notEmpty.signal();是为了激活调用 notEmpty.await()阻塞后放入 notEmpty 条件队列中的线程。
Put 操作
在队列尾部添加元素,如果队列满则等待队列有空位置插入后返回。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//获取可被中断锁
lock.lockInterruptibly();
try {
//如果队列满,则把当前线程放入 notFull 管理的条件队列
while (count == items.length){
//调用Condition的await()方法会使线程进入notFull等待队列,并释放锁,线程状态变为等待状态。
//唤醒同步队列中的后继节点,该线程中断,跳入lockInterruptibly
notFull.await();
}
//插入元素
insert(e);
} finally {
lock.unlock();
}
}
需要注意的是如果队列满了那么当前线程会阻塞,知道出队操作调用了 notFull.signal 方法激活该线程。代码逻 辑很简单,但是这里需要思考一个问题为啥调用 lockInterruptibly 方法而不是 Lock 方法。我的理解是因为调用了条件变量的 await()方法,而 await()方法会在中断标志设置后抛出 InterruptedException 异常后退出,所以还不如在加 锁时候先看中断标志是不是被设置了,如果设置了直接抛出 InterruptedException 异常,就不用再去获取锁了。然后 看了其他并发类里面凡是调用了 await 的方法获取锁时候都是使用的 lockInterruptibly 方法而不是 Lock 也验证了这 个想法。
对lockInterruptibly不理解的孩童,可以阅读这篇文章:https://blog.csdn.net/qq_31803503/article/details/87888427
对Condition不理解的孩童,可查阅:https://mp.csdn.net/postedit/87876461
Poll 操作
从队头获取并移除元素,队列为空,则返回 null。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//当前队列为空则返回 null,否者
return (count == 0) ? null : extract();
} finally {
lock.unlock();
}
}
private E extract() {
final Object[] items = this.items;
//获取元素值
E x = this.<E>cast(items[takeIndex]);
//数组中值值为 null;
items[takeIndex] = null;
//队头指针计算,队列元素个数减一
takeIndex = inc(takeIndex);
--count;
//发送信号激活 notFull 条件队列里面的线程
notFull.signal();
return x;
}
Take 操作
从队头获取元素,如果队列为空则阻塞直到队列有元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//队列为空,则等待,直到队列有元素
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
需要注意的是如果队列为空,当前线程会被挂起放到 notEmpty 的条件队列里面,直到入队操作执行调用 notEmpty.signal 后当前线程才会被激活,await 才会返回。
Peek 操作
返回队列头元素但不移除该元素,队列为空,返回 null。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//队列为空返回 null,否者返回头元素
return (count == 0) ? null : itemAt(takeIndex);
} finally {
lock.unlock();
}
}
final E itemAt(int i) {
return this.<E>cast(items[i]);
}
Size 操作
获取队列元素个数,非常精确因为计算 size 时候加了独占锁,其他线程不能入队或者出队或者删除元素。
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
ArrayBlockingQueue 小结
ArrayBlockingQueue 通过使用全局独占锁实现同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较 大,有点类似在方法上添加 synchronized 的意味。其中 offer,poll 操作通过简单的加锁进行入队出队操作,而 put,take 则使用了条件变量实现如果队列满则等待,如果队列空则等待,然后分别在出队和入队操作中发送信号激活等待线程 实现同步。另外相比 LinkedBlockingQueue,ArrayBlockingQueue 的 size 操作的结果是精确的,因为计算前加了 全局锁。
ArrayBlockingQueue 示例
需求:在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入,要求使用 java 的多线程来实现
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* 需求:在多线程操作下,一个数组中最多只能存入 3 个元素。多放入不可以存入数组,或等待某线程对数组中某个元素取走才能放入,要求使用 java 的多线程来实现。
*
* @author: JC.Lin
* @data: 2019/2/22 13:06
*/
public class BlockingQueueTest {
public static void main(String[] args) {
final BlockingQueue queue = new ArrayBlockingQueue(3);
for (int i = 0; i < 2; i++) {
new Thread() {
public void run() {
while (true) {
try {
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + "准备放数据!");
queue.put(1);
System.out.println(Thread.currentThread().getName() + "已经放了数据," + "队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
new Thread() {
public void run() {
while (true) {
try {
//将此处的睡眠时间分别改为 100 和 1000,观察运行结果
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "准备取数据!");
System.err.println(queue.take());
System.out.println(Thread.currentThread().getName() + "已经取走数据," + "队列目前有" + queue.size() + "个数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
输出结果:
Thread-2准备取数据!
1
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-1准备放数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-1已经放了数据,队列目前有0个数据
1
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
1
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2准备取数据!
1
Thread-2已经取走数据,队列目前有0个数据