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 防止虚假唤醒。

相关推荐
木木子222 小时前
# 密码强度检测深度解析:正则表达式实时分析、多维度评分算法与可视化反馈
mysql·算法·华为·正则表达式·harmonyos
大圣编程3 小时前
Java 多维数组详解
java·开发语言
Sw1zzle5 小时前
算法入门(四):二叉树 - 递归遍历三件套
算法·leetcode
万法若空5 小时前
【算法-查找】查找算法
java·数据结构·算法
海石5 小时前
子树怎么找?树的3种遍历方式来帮忙!
算法·leetcode
海石5 小时前
难度分 1588:思路 + 技巧 = AC
算法·leetcode
殳翰6 小时前
下服务器端开发流程及相关工具介绍(C++)
开发语言·c++
落寞的星星6 小时前
这个对象就包含了已经转换好的DFA和各种词法分析器运转所需要的参数。下一步,我们就可以用ScannerInfo对象创建出Scanner对象,请看下面的代码:
开发语言·c#
nothing&nowhere6 小时前
用 Python 做问卷数据清洗:无效样本检测与处理实战
开发语言·python·数据清洗·数据处理·问卷星·问卷星脚本·刷问卷
2601_961593427 小时前
Rust 开发环境配置繁琐?RustRover 开箱即用搞定编码调试
开发语言·后端·macos·rust