ArrayBlockingQueue 源码解析

ArrayBlockingQueue 源码深度分析

ArrayBlockingQueue 是 Java 并发包(java.util.concurrent)中基于数组实现的有界阻塞队列,核心设计围绕「循环队列 + 独占锁 + 条件变量」展开,保证线程安全的同时支持阻塞式入队 / 出队操作。本文将从核心特性、类结构、源码实现、关键机制等维度全面解析。

一、核心特性

  1. 有界性:底层基于固定容量的数组,初始化时必须指定容量,且后续不可修改;
  2. FIFO 顺序:遵循「先进先出」原则,入队从队尾插入,出队从队首移除;
  3. 阻塞机制:
    • 队列满时,入队线程阻塞(put 方法);队列空时,出队线程阻塞(take 方法);
    • 支持超时阻塞(offer(timeout)poll(timeout))和非阻塞(offerpoll)操作;
  4. 线程安全 :通过 ReentrantLock 独占锁保证所有操作的原子性,支持公平 / 非公平锁策略;
  5. 元素非空 :不允许插入 null 元素,否则抛出 NullPointerException
  6. 内存友好:数组结构内存连续,缓存命中率高,延迟较低。

二、类结构与核心成员变量

2.1 类继承关系

java 复制代码
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    // ... 核心代码
}
  • 继承 AbstractQueue:复用队列基础方法(如 addisEmpty);
  • 实现 BlockingQueue:提供阻塞式入队 / 出队接口;
  • 实现 Serializable:支持序列化(需注意 transient 修饰的锁和条件变量需手动序列化)。

2.2 核心成员变量

java 复制代码
// 底层存储元素的数组(final 修饰,容量不可变)
private final Object[] items;

// 出队索引:下一个要被 take/poll 的元素位置
private int takeIndex;

// 入队索引:下一个要被 put/offer 的元素位置
private int putIndex;

// 队列中当前元素个数(无需原子类,因所有操作都在锁保护下)
private int count;

// 独占锁:控制入队/出队的线程安全(公平/非公平)
private final ReentrantLock lock;

// 条件变量:队列非空时唤醒等待出队的线程
private final Condition notEmpty;

// 条件变量:队列非满时唤醒等待入队的线程
private final Condition notFull;

// 迭代器相关(弱一致性迭代器)
transient Itrs itrs = null;
  • 关键设计:
    1. takeIndex/putIndex:实现循环队列的核心,通过「取模运算」循环复用数组空间;
    2. lock:单锁设计(入队 / 出队共用同一把锁),简化锁管理但降低并发吞吐量;
    3. notEmpty/notFull:基于 Condition 的等待 - 通知机制,精准唤醒阻塞线程。

三、构造方法:初始化核心组件

ArrayBlockingQueue 提供 3 个构造方法,核心是初始化数组、锁和条件变量,并支持初始元素填充。

3.1 指定容量(默认非公平锁)

java 复制代码
public ArrayBlockingQueue(int capacity) {
    this(capacity, false); // 默认非公平锁
}

3.2 指定容量 + 公平性策略

java 复制代码
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity]; // 初始化固定容量数组
    this.lock = new ReentrantLock(fair); // 公平/非公平锁
    this.notEmpty = lock.newCondition(); // 绑定锁的条件变量
    this.notFull = lock.newCondition();
}
  • 公平性影响:
    • 公平锁(fair=true):线程获取锁的顺序遵循「等待队列顺序」,避免饥饿,但锁竞争时性能略低;
    • 非公平锁(fair=false):线程可「插队」获取锁,吞吐量更高,但可能导致部分线程饥饿。

3.3 指定容量 + 公平性 + 初始元素

java 复制代码
public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c) {
    this(capacity, fair);
    final ReentrantLock lock = this.lock;
    lock.lock(); // 初始化时加锁,避免并发问题
    try {
        int i = 0;
        try {
            for (E e : c) {
                checkNotNull(e); // 检查元素非空
                items[i++] = e; // 填充数组
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            throw new IllegalArgumentException("Collection size exceeds queue capacity");
        }
        count = i; // 设置元素个数
        putIndex = (i == capacity) ? 0 : i; // 更新入队索引(循环处理)
    } finally {
        lock.unlock();
    }
}
  • 初始化时直接加锁,确保填充元素过程中无并发干扰;
  • 若集合大小超过队列容量,抛出 IllegalArgumentException

四、核心方法源码解析

ArrayBlockingQueue 的核心逻辑集中在「入队」「出队」「删除」三大操作,均基于锁保护和循环队列实现。

4.1 入队操作

入队相关方法:put(阻塞)、offer(非阻塞 / 超时)、enqueue(私有核心实现)。

4.1.1 put(E e):阻塞入队(队列满时等待)
java 复制代码
public void put(E e) throws InterruptedException {
    checkNotNull(e); // 禁止插入 null
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly(); // 可中断锁(响应线程中断)
    try {
        // 队列满时,阻塞当前线程,等待 notFull 信号
        while (count == items.length)
            notFull.await(); // 释放锁,进入等待队列
        enqueue(e); // 核心入队逻辑
    } finally {
        lock.unlock(); // 确保锁释放
    }
}
  • 关键细节:
    • lockInterruptibly():线程在等待锁时可被中断(抛出 InterruptedException);
    • while (count == items.length):用循环而非 if,避免「虚假唤醒」(Condition 可能被无故唤醒,需重新检查条件)。
4.1.2 offer(E e):非阻塞入队(队列满时直接返回)
java 复制代码
public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 队列满则返回 false,否则入队
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}
4.1.3 offer(E e, long timeout, TimeUnit unit):超时阻塞入队
java 复制代码
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
    checkNotNull(e);
    long nanos = unit.toNanos(timeout); // 转换为纳秒
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
            if (nanos <= 0)
                return false; // 超时返回 false
            // 等待指定时间,返回剩余等待时间(<=0 表示超时)
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}
4.1.4 enqueue(E x):私有核心入队逻辑
java 复制代码
private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x; // 插入到 putIndex 位置
    // 入队索引循环:到达数组末尾则回到 0
    if (++putIndex == items.length)
        putIndex = 0;
    count++; // 元素个数+1
    notEmpty.signal(); // 唤醒等待出队的线程(队列从空变非空)
}
  • 循环队列的核心:putIndex 自增后通过 == items.length 判断是否需要「绕圈」。

4.2 出队操作

出队相关方法:take(阻塞)、poll(非阻塞 / 超时)、dequeue(私有核心实现)。

4.2.1 take():阻塞出队(队列空时等待)
java 复制代码
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 队列空时,阻塞当前线程,等待 notEmpty 信号
        while (count == 0)
            notEmpty.await();
        return dequeue(); // 核心出队逻辑
    } finally {
        lock.unlock();
    }
}
4.2.2 poll():非阻塞出队(队列空时返回 null)
java 复制代码
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 队列为空返回 null,否则出队
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}
4.2.3 dequeue():私有核心出队逻辑
java 复制代码
private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex]; // 取出 takeIndex 位置的元素
    items[takeIndex] = null; // 置空,帮助 GC
    // 出队索引循环:到达数组末尾则回到 0
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--; // 元素个数-1
    // 迭代器弱一致性处理(若有迭代器正在遍历,通知元素已出队)
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal(); // 唤醒等待入队的线程(队列从满变非满)
    return x;
}

4.3 删除操作:remove(Object o)

数组实现的队列删除非队首元素效率较低(需遍历 + 移动元素):

java 复制代码
public boolean remove(Object o) {
    if (o == null) return false;
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count > 0) {
            final int putIndex = this.putIndex;
            int i = takeIndex;
            // 循环遍历队列(从 takeIndex 到 putIndex)
            do {
                if (o.equals(items[i])) {
                    removeAt(i); // 核心删除逻辑
                    return true;
                }
                if (++i == items.length)
                    i = 0;
            } while (i != putIndex);
        }
        return false;
    } finally {
        lock.unlock();
    }
}
removeAt(int removeIndex):删除指定索引元素
java 复制代码
private void removeAt(final int removeIndex) {
    final Object[] items = this.items;
    // 情况1:删除的是当前出队索引(takeIndex)
    if (removeIndex == takeIndex) {
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
    } else {
        // 情况2:删除中间元素(需移动后续元素填补空位)
        final int putIndex = this.putIndex;
        // 循环移动元素:从 removeIndex 到 putIndex-1
        for (int i = removeIndex;;) {
            int next = i + 1;
            if (next == items.length)
                next = 0;
            if (next != putIndex) {
                items[i] = items[next];
                i = next;
            } else {
                items[i] = null;
                this.putIndex = i; // 更新入队索引(因元素前移)
                break;
            }
        }
        count--;
        if (itrs != null)
            itrs.removedAt(removeIndex);
    }
    notFull.signal(); // 删除元素后队列有空位,唤醒入队线程
}
  • 性能说明:删除中间元素时需遍历移动后续元素,时间复杂度为 O (n),不适用于高频删除非队首元素的场景。

4.4 查看操作:peek()

获取队首元素但不删除(非阻塞):

java 复制代码
public E peek() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return itemAt(takeIndex); // 直接返回 takeIndex 位置元素(可能为 null)
    } finally {
        lock.unlock();
    }
}

// 辅助方法:获取指定索引元素(队空时返回 null)
private E itemAt(int i) {
    return (E) items[i];
}

五、关键机制解析

5.1 循环队列实现

ArrayBlockingQueue 通过 takeIndexputIndexcount 三个变量实现循环队列:

  • 入队:putIndex 从 0 开始递增,到达数组长度后重置为 0;
  • 出队:takeIndex 从 0 开始递增,到达数组长度后重置为 0;
  • 队列满:count == items.length(无需通过 putIndex + 1 == takeIndex 判断,简化逻辑);
  • 队列空:count == 0

5.2 锁与条件变量的协同

  • ReentrantLock 保证入队、出队、删除等操作的原子性,同一时间只有一个线程能操作队列;
  • 条件变量:
    • notEmpty:当队列从空→非空时,通过 signal() 唤醒等待出队的线程;
    • notFull:当队列从满→非满时,通过 signal() 唤醒等待入队的线程;
  • 等待 - 通知流程:
    1. 生产者线程调用 put 时队列满 → 调用 notFull.await() 释放锁,进入等待队列;
    2. 消费者线程调用 take 出队后 → 调用 notFull.signal() 唤醒一个生产者线程;
    3. 生产者线程被唤醒后,重新获取锁,检查队列是否非满,然后入队。

5.3 弱一致性迭代器

ArrayBlockingQueue 的迭代器(iterator())是弱一致性的:

  • 迭代器创建时会快照当前队列的 takeIndexcount 等状态;
  • 迭代过程中,其他线程修改队列(如入队、出队、删除),迭代器可能不会立即反映最新状态,也不会抛出 ConcurrentModificationException
  • 核心原因:迭代器遍历期间不持有锁,仅通过 itrs 记录元素变化(如 elementDequeued()removedAt()),保证迭代过程不崩溃,但不保证数据实时性。

六、性能对比与适用场景

6.1 与 LinkedBlockingQueue 对比

特性 ArrayBlockingQueue LinkedBlockingQueue
底层结构 数组(连续内存) 链表(离散节点)
容量 有界(必须指定容量) 可选有界(默认 Integer.MAX_VALUE)
锁设计 单锁(入队 / 出队共用) 双锁(takeLock/putLock)
并发吞吐量 较低(单锁竞争) 较高(双锁减少竞争)
延迟 较低(缓存命中率高) 较高(节点分配 / 回收开销)
删除非队首元素 O (n)(数组移动) O (1)(链表节点删除)

6.2 适用场景

  • 适合「有界队列」场景(如限流、避免内存溢出);
  • 适合生产者 - 消费者模型中,元素入队 / 出队频繁、删除操作少的场景;
  • 对延迟敏感、缓存友好的场景(数组连续内存优势)。

七、注意事项

  1. 禁止插入 null :所有入队方法都会调用 checkNotNull(e),插入 null 会抛出 NullPointerException
  2. 阻塞可中断puttakeoffer(timeout)poll(timeout) 均支持线程中断,需处理 InterruptedException
  3. 公平性选择:非公平锁吞吐量更高,公平锁避免饥饿,需根据业务场景选择;
  4. 序列化 :队列支持序列化,但锁(ReentrantLock)和条件变量(Condition)是 transient 的,序列化时仅保存数组、索引、元素个数等状态,反序列化后重新初始化锁和条件变量。

总结

ArrayBlockingQueue 是「有界阻塞队列」的经典实现,核心设计是「数组循环队列 + ReentrantLock + Condition」:

  • 数组保证内存连续,提升缓存命中率;
  • 循环队列复用数组空间,避免扩容开销;
  • 单锁 + 条件变量简化线程同步,保证线程安全;
  • 支持阻塞 / 非阻塞 / 超时操作,适配不同并发场景。

其设计权衡了简单性和性能,适合需要严格限制队列大小、低延迟的生产者 - 消费者场景。

相关推荐
该用户已不存在1 小时前
6款Vibe Coding工具,让开发从从容容游刃有余
后端·aigc·ai编程
Tim_102 小时前
【C++入门】02、C++程序初识
开发语言·c++
编程修仙2 小时前
第一篇 认识SpringBoot
java·spring boot
qwepoilkjasd2 小时前
std::string详解
后端
bcbnb2 小时前
iOS 应用上架流程的工程化拆解 从签名体系到提交审核的全过程管控
后端
数新网络2 小时前
Compaction in Apache Iceberg
后端
骇客野人2 小时前
.gitignore文件常用设置
java
神奇的程序员2 小时前
实现一个内网服务监测告警系统
后端·自动化运维
lkbhua莱克瓦242 小时前
项目知识——Next.js App Router体系
开发语言·javascript·项目知识