介绍
BlockingQueue 是 Java 并发包提供的一种队列实现,它支持在队列为空或者队列满时进行阻塞操作。这种队列常用于生产者-消费者模式中,多个线程可以安全地从队列的两端添加或者移除元素。
BlockingQueue就是典型的生产者-消费者模型,它可以做到以下几点:
1.当阻塞队列数据为空时,所有的消费者线程都会被阻塞,等待队列非空。
2.当生产者往队列里填充数据后,队列就会通知消费者队列非空,消费者此时就可以进来消费。
3.当阻塞队列因为消费者消费过慢或者生产者存放元素过快导致队列填满时无法容纳新元素时,生产者就会被阻塞,等待队列非满时继续存放元素。
4.当消费者从队列中消费一个元素之后,队列就会通知生产者队列非满,生产者可以继续填充数据了。
常用API
public interface BlockingQueue<E> extends Queue<E> {
// 1.添加元素
// 插入元素到队列中,如果队列满了则抛出异常
boolean add(E e);
// 插入元素到队列中,如果队列已满则返回 false
boolean offer(E e);
// 插入元素到队列中,如果队列已满则阻塞等待直到有空间可用
void put(E e) throws InterruptedException;
// 插入元素到队列中,在指定的超时时间内等待队列有空间可用
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
// 2.获取元素
// 移除并返回队列头部的元素,如果队列为空则阻塞等待直到有元素可取
E take() throws InterruptedException;
// 移除并返回队列头部的元素,在指定的超时时间内等待队列有元素可取
E poll(long timeout, TimeUnit unit) throws InterruptedException;
// 3.其他方法
// 返回队列中剩余可用空间的大小
int remainingCapacity();
// 从队列中移除指定元素
boolean remove(Object o);
// 判断队列是否包含指定元素
boolean contains(Object o);
// 从队列中移除并返回元素到指定集合中
int drainTo(Collection<? super E> c);
// 从队列中最多移除指定数量的元素到指定集合中
int drainTo(Collection<? super E> c, int maxElements);
}
ArrayBlockingQueue
现在让我们以ArrayBlockingQueue为例来学习BlockingQueue的底层原理.
示例
现在看下面代码示例
public static void main(String[] args) throws InterruptedException {
// 定义一个队列QUEUE,最大元素为5
BlockingQueue<Integer> QUEUE = new ArrayBlockingQueue<Integer>(5);
// 生产者线程:往QUEUE中依次放入10个元素
Thread producer = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
try {
QUEUE.put(i);
System.out.println("队列中放入元素:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
CountDownLatch countDownLatch = new CountDownLatch(1);
// 消费者线程:从QUEUE中获取元素
Thread consumer = new Thread(() -> {
int count = 0;
while (true) {
try {
Integer i = QUEUE.take();
System.out.println("队列中取出元素:" + i);
count++;
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 当获取到10个的时候 -> 退出循环
if (count == 10) {
break;
}
}
countDownLatch.countDown();
});
producer.start();
Thread.sleep(100);
consumer.start();
countDownLatch.await();
System.out.println("done~");
}
// 输出结果
队列中放入元素:1
队列中放入元素:2
队列中放入元素:3
队列中放入元素:4
队列中放入元素:5
队列中取出元素:1
队列中放入元素:6
队列中取出元素:2
队列中放入元素:7
队列中取出元素:3
队列中放入元素:8
队列中取出元素:4
队列中放入元素:9
队列中放入元素:10
队列中取出元素:5
队列中取出元素:6
队列中取出元素:7
队列中取出元素:8
队列中取出元素:9
队列中取出元素:10
done~
// 总结
如上所说,队列中最多放入5个元素,当QUEUE满了之后,只有等待消费者去获取元素后,生产者线程才能继续往队列中添加元素.
这也就意味着当队列中没有数据的时消费者就会阻塞,等待队列非空再继续消费。
底层实现
现在我们来一起探究下ArrayBlockingQueue底层是怎么实现的?
初始化队列
初始化了 ArrayBlockingQueue 内部的底层数组、锁对象(用的是ReentrantLock)以及两个条件变量,用于控制在队列不为空或不满时的等待和通知机制
BlockingQueue<Integer> QUEUE = new ArrayBlockingQueue<Integer>(5);
// -> 查看底层构造方法
public ArrayBlockingQueue(int capacity, boolean fair) {
// 检查容量是否合法,如果小于等于0则抛出非法参数异常
if (capacity <= 0)
throw new IllegalArgumentException();
// 初始化队列的底层数组,并指定容量大小
this.items = new Object[capacity];
// 创建可重入锁对象,可以选择是否公平竞争
lock = new ReentrantLock(fair);
// 创建条件变量用于在队列不为空时等待
notEmpty = lock.newCondition();
// 创建条件变量用于在队列不满时等待
notFull = lock.newCondition();
}
// capacity
参数表示队列的容量大小,如果小于等于0则会抛出非法参数异常。
// lock
是用于同步的可重入锁对象,这里可以选择是否使用公平竞争。
// notEmpty
条件变量,在队列不为空时等待。
// notFull
条件变量,在队列不满时等待
下图为条件队列视图,条件队列是个单向链表
put()
先来看下put()大致流程
public void put(E e) throws InterruptedException {
// 检查插入的元素是否为空,如果为空则抛出空指针异常
checkNotNull(e);
// 获取队列的锁对象
final ReentrantLock lock = this.lock;
// 获取锁,如果线程在获取锁的过程中被中断,则抛出中断异常
lock.lockInterruptibly();
try {
// 如果队列已满,线程进入等待状态直到队列不满
while (count == items.length)
notFull.await();
// 将元素插入队列
enqueue(e);
} finally {
// 最终释放锁
lock.unlock();
}
}
// 大致逻辑
1.检查插入的元素是否为空,如果为空则抛出空指针异常。
2.获取队列的可重入锁对象,并在获取锁的过程中响应中断,如果线程在获取锁的过程中被中断,则抛出中断异常。
3.如果队列已满,线程进入等待状态 -> notFull.await() 。
4.若队列未满,将元素插入队列 -> enqueue(e);
5.最终释放锁,确保对锁的正确释放
接着来看notFull.await()
// notFull.await()
public final void await() throws InterruptedException {
// 如果当前线程被中断,则抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程添加到条件等待队列,并返回对应的节点
Node node = addConditionWaiter();
// 完全释放当前节点所持有的所有锁,并保存释放前的状态
int savedState = fullyRelease(node);
// 用于记录中断的模式
int interruptMode = 0;
// 循环检查节点是否在同步队列中
while (!isOnSyncQueue(node)) {
// 将当前线程挂起(阻塞)
LockSupport.park(this);
// 检查在等待期间是否被中断
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 尝试获取锁,返回 true 表示成功获取,返回 false 表示获取失败
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果等待队列中还有其他等待节点,清理被取消的等待节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 如果中断模式不为 0,报告等待后的中断情况
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 这里描述上述代码例子的简单场景
当生产者线程t1来queue中存放元素,发现此时队列满了,
t1会释放ReentrantLock锁,
并且t1会进入条件等待队列noFull,t1进入阻塞状态,
等待后续noEmpty队列唤醒
现在回过头来看下:如果生产者put时,队列没有满,则会走存放元素的逻辑
// enqueue(e)
private void enqueue(E x) {
// 获取队列中的底层数组
final Object[] items = this.items;
// 将元素放入队列中的指定位置
items[putIndex] = x;
// 更新 putIndex 指向下一个可插入元素的位置
if (++putIndex == items.length)
putIndex = 0;
// 队列元素数量增加
count++;
// 发送信号,唤醒可能在等待队列不为空时等待的线程
notEmpty.signal();
}
// 大致流程
它的作用是向队列中的 putIndex 位置放入新元素,
并根据情况更新 putIndex 的位置,然后增加队列中元素的数量。
最后通过 notEmpty.signal() 发送信号,唤醒可能在等待队列不为空时等待的线程。
take()
看完了put(),我们再来欣赏下take()
public E take() throws InterruptedException {
// 获取队列的可重入锁对象
final ReentrantLock lock = this.lock;
// 获取锁,如果线程在获取锁的过程中被中断,则抛出中断异常
lock.lockInterruptibly();
try {
// 如果队列为空,线程进入等待状态直到队列不为空
while (count == 0)
notEmpty.await();
// 从队列中取出元素
return dequeue();
} finally {
// 最终释放锁
lock.unlock();
}
}
// 大致逻辑
1.获取队列的可重入锁对象 获取锁,如果线程在获取锁的过程中被中断,则抛出中断异常。
2.若此时如果队列为空(count == 0),线程会进入等待状态-> notEmpty.await()
3.当队列不为空时,调用 dequeue() 方法从队列中取出元素。
4.最后释放锁
// notEmpty.await()
类似notFull.await(),大致逻辑为:
将消费者线程放入条件等待队列notFull,释放当前线程持有的锁,线程进入阻塞,等待唤醒.
现在重点来看下元素出队的逻辑
// dequeue()
private E dequeue() {
// 获取队列中的底层数组
final Object[] items = this.items;
// 从队列中取出元素
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
// 清空取出元素的位置
items[takeIndex] = null;
// 更新 takeIndex 指向下一个待取元素的位置
if (++takeIndex == items.length)
takeIndex = 0;
// 队列元素数量减少
count--;
// 如果存在迭代器,通知元素已经出队
if (itrs != null)
itrs.elementDequeued();
// 发送信号,唤醒可能在等待队列不满时等待的线程
notFull.signal();
// 返回取出的元素
return x;
}
承接上述put(),当队列中有元素被拿走,会去唤醒等待队列notFull中的生产者线程
// notFull.signal()
public final void signal() {
// 如果当前线程不是独占锁的持有者,则抛出非法监视器状态异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取等待队列中的第一个等待节点
Node first = firstWaiter;
// 如果第一个等待节点不为空,则执行信号唤醒操作
if (first != null)
doSignal(first);
}
// -> doSignal(first);
private void doSignal(Node first) {
do {
// 移动等待队列中的第一个等待节点,更新 firstWaiter 和 lastWaiter 指向
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 将 first 的下一个等待节点置为空,用于后续节点处理
first.nextWaiter = null;
// 如果信号传递失败,并且等待队列中仍有等待节点,则继续处理下一个等待节点
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// ->transferForSignal(first)
final boolean transferForSignal(Node node) {
/*
* 如果无法修改等待状态,则节点已被取消。
* 尝试将节点的等待状态从 CONDITION 修改为 0,如果失败则返回 false。
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 将节点加入队列,并尝试设置前驱节点的等待状态,指示线程(可能)正在等待。
* 如果前驱节点被取消,或者尝试设置其等待状态失败,则唤醒以重新同步。
* 在这种情况下,等待状态可能会短暂地出现错误,但不会有实际影响。
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
// 返回 true 表示成功传递信号
return true;
}
// 大致逻辑
1.当已满的队列有元素被拿走,则说明队列有空位了,则需要通知唤醒条件等待队列中的生产者线程
2.获取条件队列中的节点,将节点等待状态修改为0(正常CLH队列的节点状态)
3.将该节点放入CLH队列,等待获取reentantLock锁
4.获取到锁后,便会继续执行存放元素的逻辑.
总结
// 相关总结
1.单机场景下,同一时间只能有一个线程来操作BlockingQueue,必须先获取到锁资源ReentrantLock,若没有获取到则进入CLH队列(参考ReentrantLock底层原理)
2.put()元素时,若队列满了,则该线程进入条件队列NotFull,阻塞等待唤醒.
3.take()元素时,若队列为空,则进入条件队列NotEmpty,阻塞等待唤醒.
4.唤醒后的线程,不会立刻执行,需要获取到ReentrantLock才行,所以唤醒后到线程可能会进入CLH队列继续阻塞等待.