Java并发编程:深入剖析 ArrayBlockingQueue

1. 引言:为什么需要 ArrayBlockingQueue?

在Java并发编程中,生产者-消费者模式是一种非常经典的解耦设计。而阻塞队列 正是这一模式的核心组件。ArrayBlockingQueue 作为JUC包中一个重要的有界阻塞队列实现,它通过数组 存储数据,并利用 ReentrantLockCondition 实现了线程安全的阻塞存取操作。

本文将带你从类图、核心属性、构造器,到 put/takeoffer/poll 的核心源码,逐行分析其背后原理,并解答一个关键问题:为什么使用 while 而不是 if 进行条件判断


2. 核心结构与类图解析

我们先通过一个简化的类图,一览 ArrayBlockingQueue 的内部骨架:

复制代码

关键点说明:

  • items、takeIndex、putIndex、count 等变量没有使用 volatile,因为所有读写都在锁保护范围内,锁已经保证了内存可见性。

  • lock 是全局独占锁,同一时刻只允许一个线程执行入队或出队操作(读写互斥)。

  • notEmptynotFull 是两个条件变量,用于线程间的等待与唤醒。


3. 构造器:初始化队列与锁

java 复制代码
public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

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();
}
  • capacity 必须 > 0,数组大小一旦确定就不能扩容(有界)。

  • fair 参数决定锁是否为公平锁。公平锁能避免线程饥饿,但会降低吞吐量。


4. 入队操作源码分析

4.1 offer(e) ------ 非阻塞入队

java 复制代码
public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

流程解析

  1. 加锁:保证线程安全。

  2. 队列满 :直接返回 false,不阻塞。

  3. 队列未满 :调用 enqueue 真正入队。

  4. 解锁 :释放锁,修改后的变量(如 count)会立即刷回主内存。

优点:快速失败,适合高并发下不希望等待的场景。

4.2 enqueue ------ 真正的入队逻辑

java 复制代码
private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;              // 放置元素
    if (++putIndex == items.length)   // 环形数组
        putIndex = 0;
    count++;                          // 元素个数+1
    notEmpty.signal();                // 唤醒一个等待取元素的线程
}

环形数组

  • 通过 putIndex 自增并取模(== length 时重置为0),实现数组的循环利用。

  • putIndex 到达数组末尾时,下次插入会从0开始。

信号唤醒

  • 每次成功入队后,都会调用 notEmpty.signal(),通知可能正在等待"队列非空"的消费者线程。

4.3 put(e) ------ 可阻塞入队

java 复制代码
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();
    }
}

为什么用 lockInterruptibly()

  • 允许线程在等待锁的过程中响应中断,抛出 InterruptedException,这是一种优雅的退出机制。

为什么是 while 而不是 if

  • 防止虚假唤醒 。线程可能在未真正被 signal 的情况下醒来(spurious wakeup),用 while 重新检查条件,确保队列真的未满才继续执行。

5. 出队操作源码分析

5.1 poll() ------ 非阻塞出队

java 复制代码
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}
  • 队列为空直接返回 null,不阻塞。

  • 否则调用 dequeue 取出头元素。

5.2 dequeue ------ 真正的出队逻辑

java 复制代码
private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;                 // 帮助GC
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    notFull.signal();                        // 唤醒一个等待放入的线程
    return x;
}

细节分析

  • 取出 takeIndex 位置的元素,并置为 null,让GC及时回收。

  • 更新 takeIndexcount

  • notFull.signal():队列空出一个位置,通知可能正在等待"队列未满"的生产者。

5.3 take() ------ 可阻塞出队

java 复制代码
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}
  • while(count == 0) :如果队列为空,当前线程进入 notEmpty 条件队列等待。

  • poll 的唯一区别take 会在队列空时阻塞,而不是立即返回 null


6. 条件变量与等待通知机制图解

复制代码
生产者线程                    消费者线程
    |                           |
 put() 队列满                take() 队列空
    |                           |
 notFull.await()            notEmpty.await()
    |                           |
  等待                        等待
    |                           |
 dequeue() 出队              enqueue() 入队
    |                           |
 notFull.signal()            notEmpty.signal()
  • notFull:队列满时,生产者等待。

  • notEmpty:队列空时,消费者等待。

  • 每次成功的 enqueue 会唤醒一个消费者,每次成功的 dequeue 会唤醒一个生产者。


7. 与 LinkedBlockingQueue 的对比

特性 ArrayBlockingQueue LinkedBlockingQueue
数据结构 数组 链表
是否无界 有界(必须指定容量) 可选有界/无界
锁粒度 一个锁(读写互斥) 两个锁(读写分离)
内存效率 更好(连续内存) 一般
吞吐量 较低(锁竞争大) 较高
size() 精确度 精确(锁内计算) 精确(但基于原子变量)

8. 最佳实践与注意事项

  1. 选择合适的容量ArrayBlockingQueue 是有界的,容量过小会导致频繁阻塞,过大则浪费内存。

  2. 公平锁 vs 非公平锁:默认非公平锁性能更高,但公平锁可避免线程饥饿。

  3. 使用 offerpoll 代替 put/take:在非阻塞或超时场景下,避免线程长时间挂起。

  4. 正确处理中断 :使用 lockInterruptibly() 并捕获 InterruptedException,在取消任务时及时释放资源。


9. 总结

ArrayBlockingQueue 虽然名字简单,但背后蕴含了并发编程中的核心思想:

  • 保证原子性与可见性。

  • 条件变量 实现高效的等待/通知机制。

  • 环形数组 实现内存复用。

  • while 循环 + await 防止虚假唤醒。

相关推荐
君为先-bey1 小时前
Latte——视频生成的潜在扩散变换器
算法·机器学习·音视频·扩散模型
吃好睡好便好1 小时前
提取矩阵所有元素
开发语言·学习·线性代数·matlab·矩阵
浅念-1 小时前
LeetCode刷题专题:FloodFill泛滥填充算法剖析
数据结构·算法·leetcode·职场和发展·深度优先·宽度优先
吃好睡好便好1 小时前
提取矩阵特定多列元素
开发语言·学习·线性代数·matlab·矩阵
yujunl1 小时前
MES系统的悟道过程
开发语言
菜菜的顾清寒1 小时前
力扣HOT100(33)二叉树的最大深度
算法·leetcode·职场和发展
小郑加油1 小时前
python_综合训练
开发语言·python
多彩电脑1 小时前
Kivy的事件向方法传递的event是什么?
开发语言·python
Refrain_zc1 小时前
Android 封装 BaseMultipleChoiceAdapter 快速实现列表多选编辑
java