ArrayBlockingQueue是Java并发包中基于循环数组实现的有界阻塞队列,通过独占锁与条件变量机制实现线程安全,核心设计围绕"生产者-消费者"模型展开。其源码展现了并发控制的经典范式:用ReentrantLock保证操作原子性,用Condition实现线程间等待/唤醒,用双指针维护循环数组的高效复用。
一、核心结构与初始化
1. 底层存储与控制变量
final Object[] items; // 存储元素的定长数组
int takeIndex; // 下一次出队索引
int putIndex; // 下一次入队索引
int count; // 当前元素数量
final ReentrantLock lock; // 全局独占锁
private final Condition notEmpty; // 队列非空条件(消费者等待)
private final Condition notFull; // 队列未满条件(生产者等待)
- 循环数组:通过
takeIndex和putIndex指针循环复用数组空间,避免元素搬迁。 - 单一锁设计:所有操作共享同一把锁,读写互斥,不同于
LinkedBlockingQueue的分离锁。
2. 初始化机制
必须指定容量,支持公平性选择:
public ArrayBlockingQueue(int capacity, boolean fair) {
this.items = new Object[capacity];
lock = new ReentrantLock(fair); // 公平锁保证FIFO访问顺序
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
- 容量不可变:数组长度在构造时确定,后续无法扩容。
- 公平性权衡:公平锁通过AQS队列保证线程调度顺序,但吞吐量降低约30%。
二、核心操作的实现逻辑
1. 入队操作(以put()为例)
public void put(E e) throws InterruptedException {
checkNotNull(e); // 不允许null元素
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 可中断加锁
try {
while (count == items.length) // 循环检查防止虚假唤醒
notFull.await(); // 队列满时阻塞
enqueue(e); // 入队核心逻辑
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0; // 循环到数组头部
count++;
notEmpty.signal(); // 唤醒等待的消费者线程
}
- 阻塞机制:队列满时通过
notFull.await()释放锁并挂起,等待消费者唤醒。 - 循环复用:
putIndex到达数组尾部后重置为0,实现"环形缓冲区"效果。
2. 出队操作(以take()为例)
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) // 循环检查队列是否为空
notEmpty.await(); // 队列为空时阻塞
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
E x = (E) items[takeIndex];
items[takeIndex] = null; // 帮助GC
if (++takeIndex == items.length) takeIndex = 0;
count--;
notFull.signal(); // 唤醒等待的生产者线程
return x;
}
- 线程协作:出队后通过
notFull.signal()唤醒生产者,形成"生产-消费"信号闭环。 - 内存可见性:无需
volatile修饰共享变量,因为锁操作保证了内存屏障效果。
三、方法家族与行为对比
| 操作类型 | 方法 | 队列满/空时行为 | 阻塞特性 |
|---|---|---|---|
| 入队 | add(E) |
满时抛出IllegalStateException |
非阻塞 |
| 入队 | offer(E) |
满时返回false |
非阻塞 |
| 入队 | put(E) |
满时阻塞直到有空间 | 可中断阻塞 |
| 入队 | offer(E, timeout) |
满时阻塞指定时间后返回false |
限时阻塞 |
| 出队 | remove() |
空时抛出NoSuchElementException |
非阻塞 |
| 出队 | poll() |
空时返回null |
非阻塞 |
| 出队 | take() |
空时阻塞直到有元素 | 可中断阻塞 |
| 出队 | poll(timeout) |
空时阻塞指定时间后返回null |
限时阻塞 |
- 核心差异:
put()/take()是阻塞版本,offer()/poll()是非阻塞版本,add()/remove()在操作失败时抛出异常。
四、面试核心考点解析
1. 与LinkedBlockingQueue的关键区别
| 维度 | ArrayBlockingQueue |
LinkedBlockingQueue |
|---|---|---|
| 底层结构 | 循环数组 | 双向链表 |
| 容量特性 | 必须指定容量(有界) | 可选容量(默认Integer.MAX_VALUE) |
| 锁设计 | 单锁+双条件 | 分离锁(takeLock/putLock) |
| 内存开销 | 连续内存空间,无指针开销 | 节点额外存储前后指针 |
| 吞吐量 | 低(单锁竞争) | 高(读写分离) |
2. 公平锁与非公平锁的实现
- 非公平锁(默认):线程直接尝试获取锁,失败后入队,可能导致饥饿。
- 公平锁:通过
ReentrantLock(fair=true)保证线程按请求顺序获取锁,降低吞吐量但避免饥饿。
3. 为什么notEmpty.await()要用while循环?
防止虚假唤醒(spurious wakeup):线程可能在未收到signal()时被唤醒,循环检查可确保只有满足条件时才继续执行。
4. 如何实现高效的迭代器?
内部通过Itrs类维护迭代器状态,出队时调用itrs.elementDequeued()更新迭代器,避免并发修改异常。
五、使用场景与性能优化
1. 最佳适用场景
- 生产者-消费者模型:如线程池任务队列、日志收集器。
- 有界缓冲场景:需限制内存占用的高频读写场景。
- 低延迟要求:数组的连续内存访问比链表缓存友好。
2. 性能优化建议
- 预设合理容量:避免频繁阻塞唤醒(如
new ArrayBlockingQueue<>(1024))。 - 批量操作优先:使用
addAll()/drainTo()减少锁竞争次数。 - 读写分离考虑:若读写并发高,可替换为
LinkedBlockingQueue或ConcurrentLinkedQueue。
六、总结:并发编程的经典范式
ArrayBlockingQueue通过循环数组+单一锁+条件变量的组合,实现了高效的有界阻塞队列。其设计体现了并发编程的核心思想:
- 管程模型:用锁和条件变量实现线程间同步。
- 空间复用:循环数组避免动态扩容开销。
- 权衡取舍:公平性与吞吐量的平衡,阻塞与非阻塞API的选择。
理解其源码不仅能掌握阻塞队列的实现原理,更能深入领会Java并发工具的设计哲学——如何用最小的同步开销换取最大的并发安全性。