CC 咖啡猫的工作空间 Coding Space

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;   // 队列未满条件(生产者等待)
  • 循环数组:通过takeIndexputIndex指针循环复用数组空间,避免元素搬迁。
  • 单一锁设计:所有操作共享同一把锁,读写互斥,不同于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()减少锁竞争次数。
  • 读写分离考虑:若读写并发高,可替换为LinkedBlockingQueueConcurrentLinkedQueue

六、总结:并发编程的经典范式

ArrayBlockingQueue通过循环数组+单一锁+条件变量的组合,实现了高效的有界阻塞队列。其设计体现了并发编程的核心思想:

  1. 管程模型:用锁和条件变量实现线程间同步。
  2. 空间复用:循环数组避免动态扩容开销。
  3. 权衡取舍:公平性与吞吐量的平衡,阻塞与非阻塞API的选择。

理解其源码不仅能掌握阻塞队列的实现原理,更能深入领会Java并发工具的设计哲学——如何用最小的同步开销换取最大的并发安全性。