深度剖析 Java ArrayBlockingQueue:源码级的原理探秘

深度剖析 Java ArrayBlockingQueue:源码级的原理探秘

一、引言

在 Java 并发编程的领域中,队列是一种极为关键的数据结构,它在多线程环境下能够实现线程间的高效协作与数据传递。ArrayBlockingQueue 作为 Java 并发包(java.util.concurrent)里的重要成员,是一个基于数组实现的有界阻塞队列。它不仅遵循先进先出(FIFO)的原则,还具备线程安全的特性,能在队列满或空时进行阻塞操作,从而有效控制线程的执行顺序和资源的使用。

对于开发者而言,深入理解 ArrayBlockingQueue 的使用原理,不仅有助于在实际项目中更加合理地运用它,还能提升对 Java 并发编程的理解和掌握程度。本文将从源码层面出发,对 ArrayBlockingQueue 的内部结构、核心方法、线程安全机制等方面展开详尽的分析,为你揭开 ArrayBlockingQueue 的神秘面纱。

二、ArrayBlockingQueue 概述

2.1 基本概念

ArrayBlockingQueue 是一个基于数组实现的有界阻塞队列,其"有界"意味着在创建队列时需要指定一个固定的容量,当队列达到这个容量后,再往队列中插入元素会导致线程阻塞,直至队列中有元素被移除。"阻塞"则表示当队列为空时,尝试从队列中获取元素的线程会被阻塞,直到队列中有新元素加入。它严格遵循先进先出(FIFO)的原则,即最先进入队列的元素会最先被取出。

2.2 继承关系与接口实现

下面是 ArrayBlockingQueue 类的定义以及它的继承关系和接口实现:

java 复制代码
// ArrayBlockingQueue 继承自 AbstractQueue 类,AbstractQueue 是一个抽象类,实现了 Queue 接口的部分方法
// 同时,ArrayBlockingQueue 实现了 BlockingQueue 接口,表明它是一个阻塞队列
// 还实现了 Serializable 接口,说明它可以被序列化
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
    // 类的具体实现将在后续详细分析
}

从上述代码可以看出,ArrayBlockingQueue 继承自 AbstractQueue 类,继承了该类中实现的 Queue 接口的部分方法。同时,它实现了 BlockingQueue 接口,这使得它具备了阻塞队列的特性,支持在队列满或空时进行阻塞操作。此外,它还实现了 Serializable 接口,支持对象的序列化。

2.3 与其他队列的对比

在 Java 中,存在多种队列实现,如 LinkedBlockingQueuePriorityBlockingQueue 等,它们与 ArrayBlockingQueue 的主要区别如下:

  • LinkedBlockingQueue :是一个基于链表实现的有界阻塞队列,队列的容量可以在创建时指定,也可以不指定(默认容量为 Integer.MAX_VALUE)。而 ArrayBlockingQueue 基于数组实现,在创建时必须指定容量,且在整个生命周期中容量不可变。
  • PriorityBlockingQueue :是一个基于优先级堆实现的无界阻塞队列,元素会根据优先级进行排序,每次取出的元素是优先级最高的元素。而 ArrayBlockingQueue 是按照元素的插入顺序进行操作,不考虑元素的优先级。

三、ArrayBlockingQueue 的内部结构

3.1 核心属性

ArrayBlockingQueue 类有几个核心属性,用于存储元素和管理队列的状态。以下是这些核心属性的源码及注释:

java 复制代码
// 用于存储队列元素的数组
final Object[] items;

// 下一次出队操作的索引位置
int takeIndex;

// 下一次入队操作的索引位置
int putIndex;

// 队列中当前元素的数量
int count;

// 用于控制队列操作的锁
final ReentrantLock lock;

// 当队列为空时,等待元素加入的条件
private final Condition notEmpty;

// 当队列已满时,等待元素移除的条件
private final Condition notFull;
  • items:是一个 Object 类型的数组,用于存储队列中的元素。数组的长度在创建队列时指定,且在队列的整个生命周期内保持不变。
  • takeIndex:表示下一次出队操作的索引位置。当进行出队操作时,会从该索引位置取出元素,并更新 takeIndex 的值。
  • putIndex:表示下一次入队操作的索引位置。当进行入队操作时,会将元素放入该索引位置,并更新 putIndex 的值。
  • count:记录队列中当前元素的数量。通过该变量可以判断队列是否为空或已满。
  • lock:是一个 ReentrantLock 类型的锁,用于保证在多线程环境下对队列的操作是线程安全的。所有的入队和出队操作都需要先获取该锁。
  • notEmptynotFull:分别是与 lock 关联的条件对象。notEmpty 用于在队列为空时,使尝试出队的线程等待;notFull 用于在队列已满时,使尝试入队的线程等待。

3.2 构造函数

ArrayBlockingQueue 类提供了多个构造函数,用于创建不同初始状态的阻塞队列。以下是几个主要构造函数的源码及注释:

java 复制代码
// 创建一个指定容量的阻塞队列
public ArrayBlockingQueue(int capacity) {
    // 调用另一个构造函数,指定容量,并使用非公平锁
    this(capacity, false);
}

// 创建一个指定容量和是否公平的阻塞队列
public ArrayBlockingQueue(int capacity, boolean fair) {
    // 检查容量是否小于等于 0,如果是,则抛出 IllegalArgumentException 异常
    if (capacity <= 0)
        throw new IllegalArgumentException();
    // 初始化存储元素的数组,长度为指定的容量
    this.items = new Object[capacity];
    // 创建一个可重入锁,根据 fair 参数决定是否使用公平锁
    lock = new ReentrantLock(fair);
    // 创建与锁关联的 notEmpty 条件对象
    notEmpty = lock.newCondition();
    // 创建与锁关联的 notFull 条件对象
    notFull =  lock.newCondition();
}

// 创建一个包含指定集合元素的阻塞队列,容量为集合的大小
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) {
                // 检查元素是否为 null,如果是,则抛出 NullPointerException 异常
                if (e == null)
                    throw new NullPointerException();
                // 将元素放入数组中
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            // 如果集合元素数量超过队列容量,抛出 IllegalStateException 异常
            throw new IllegalStateException();
        }
        // 更新队列中元素的数量
        count = i;
        // 更新 putIndex 的值,如果 i 等于容量,则将 putIndex 置为 0
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        // 解锁
        lock.unlock();
    }
}

这些构造函数提供了多种方式来创建 ArrayBlockingQueueArrayBlockingQueue(int capacity) 构造函数创建一个指定容量的阻塞队列,使用非公平锁;ArrayBlockingQueue(int capacity, boolean fair) 构造函数创建一个指定容量和是否公平的阻塞队列;ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c) 构造函数创建一个包含指定集合元素的阻塞队列,容量为集合的大小。

3.3 数组结构

ArrayBlockingQueue 使用数组来存储队列中的元素,通过 takeIndexputIndex 两个索引来管理数组的头部和尾部。当有新元素加入队列时,会将元素放入 putIndex 所指向的位置,并更新 putIndex 的值;当有元素从队列中移除时,会从 takeIndex 所指向的位置取出元素,并更新 takeIndex 的值。

以下是一个简单的示例,展示了数组结构的工作原理:

java 复制代码
// 创建一个容量为 5 的 ArrayBlockingQueue
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
// 向队列中添加元素
queue.add("Element 1");
queue.add("Element 2");
queue.add("Element 3");

// 从队列中移除元素
String element = queue.poll();
System.out.println("Removed element: " + element);

在上述示例中,创建了一个容量为 5 的 ArrayBlockingQueue,并向队列中添加了 3 个元素。然后从队列中移除一个元素并打印。通过数组结构,ArrayBlockingQueue 可以高效地进行元素的插入和删除操作。

四、基本操作的源码分析

4.1 插入操作

4.1.1 put(E e) 方法

put(E e) 方法用于将元素插入到队列的尾部,如果队列已满,则当前线程会被阻塞,直到队列中有空间可用。以下是该方法的源码及注释:

java 复制代码
// 将元素插入到队列的尾部,如果队列已满,则阻塞当前线程
public void put(E e) throws InterruptedException {
    // 检查元素是否为 null,如果是,则抛出 NullPointerException 异常
    checkNotNull(e);
    // 获取锁
    final ReentrantLock lock = this.lock;
    // 允许线程在等待过程中被中断
    lock.lockInterruptibly();
    try {
        // 当队列已满时,当前线程在 notFull 条件上等待
        while (count == items.length)
            notFull.await();
        // 将元素插入到队列中
        enqueue(e);
    } finally {
        // 解锁
        lock.unlock();
    }
}

// 检查元素是否为 null 的方法
private static void checkNotNull(Object v) {
    if (v == null)
        throw new NullPointerException();
}

// 将元素插入到队列中的方法
private void enqueue(E x) {
    // 将元素放入 putIndex 所指向的位置
    final Object[] items = this.items;
    items[putIndex] = x;
    // 更新 putIndex 的值,如果达到数组末尾,则将其置为 0
    if (++putIndex == items.length)
        putIndex = 0;
    // 增加队列中元素的数量
    count++;
    // 唤醒一个在 notEmpty 条件上等待的线程
    notEmpty.signal();
}
  • 首先,调用 checkNotNull 方法检查插入的元素是否为 null,如果为 null,抛出 NullPointerException 异常。
  • 然后,获取锁,并在队列已满时,当前线程在 notFull 条件上等待。
  • 当队列有空间时,调用 enqueue 方法将元素插入到队列中。
  • enqueue 方法中,将元素放入 putIndex 所指向的位置,并更新 putIndex 的值。如果 putIndex 达到数组末尾,则将其置为 0,实现循环数组的效果。
  • 增加队列中元素的数量,并唤醒一个在 notEmpty 条件上等待的线程。
  • 最后,解锁。
4.1.2 offer(E e) 方法

offer(E e) 方法用于将元素插入到队列的尾部,如果队列已满,则返回 false,否则返回 true。以下是该方法的源码及注释:

java 复制代码
// 将元素插入到队列的尾部,如果队列已满,则返回 false
public boolean offer(E e) {
    // 检查元素是否为 null,如果是,则抛出 NullPointerException 异常
    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();
    }
}
  • 首先,调用 checkNotNull 方法检查插入的元素是否为 null,如果为 null,抛出 NullPointerException 异常。
  • 然后,获取锁,并检查队列是否已满。如果队列已满,直接返回 false
  • 如果队列有空间,调用 enqueue 方法将元素插入到队列中,并返回 true
  • 最后,解锁。
4.1.3 offer(E e, long timeout, TimeUnit unit) 方法

offer(E e, long timeout, TimeUnit unit) 方法用于将元素插入到队列的尾部,如果队列已满,则当前线程会等待指定的时间,如果在指定时间内队列有空间可用,则插入元素并返回 true,否则返回 false。以下是该方法的源码及注释:

java 复制代码
// 将元素插入到队列的尾部,如果队列已满,则等待指定的时间
public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
    // 检查元素是否为 null,如果是,则抛出 NullPointerException 异常
    checkNotNull(e);
    // 将超时时间转换为纳秒
    long nanos = unit.toNanos(timeout);
    // 获取锁
    final ReentrantLock lock = this.lock;
    // 允许线程在等待过程中被中断
    lock.lockInterruptibly();
    try {
        // 当队列已满时,进入循环
        while (count == items.length) {
            // 如果超时时间已到,返回 false
            if (nanos <= 0)
                return false;
            // 当前线程在 notFull 条件上等待指定的时间
            nanos = notFull.awaitNanos(nanos);
        }
        // 将元素插入到队列中
        enqueue(e);
        return true;
    } finally {
        // 解锁
        lock.unlock();
    }
}
  • 首先,调用 checkNotNull 方法检查插入的元素是否为 null,如果为 null,抛出 NullPointerException 异常。
  • 将超时时间转换为纳秒。
  • 获取锁,并在队列已满时,当前线程在 notFull 条件上等待指定的时间。
  • 如果在指定时间内队列有空间可用,调用 enqueue 方法将元素插入到队列中,并返回 true
  • 如果超时时间已到,队列仍然已满,则返回 false
  • 最后,解锁。

4.2 删除操作

4.2.1 take() 方法

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();
    }
}

// 从队列中移除元素的方法
private E dequeue() {
    // 获取数组
    final Object[] items = this.items;
    // 获取 takeIndex 所指向的元素
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    // 将该位置的元素置为 null
    items[takeIndex] = null;
    // 更新 takeIndex 的值,如果达到数组末尾,则将其置为 0
    if (++takeIndex == items.length)
        takeIndex = 0;
    // 减少队列中元素的数量
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    // 唤醒一个在 notFull 条件上等待的线程
    notFull.signal();
    return x;
}
  • 首先,获取锁,并在队列为空时,当前线程在 notEmpty 条件上等待。
  • 当队列中有元素时,调用 dequeue 方法从队列中移除元素。
  • dequeue 方法中,获取 takeIndex 所指向的元素,并将该位置的元素置为 null
  • 更新 takeIndex 的值,如果 takeIndex 达到数组末尾,则将其置为 0,实现循环数组的效果。
  • 减少队列中元素的数量,并唤醒一个在 notFull 条件上等待的线程。
  • 最后,返回移除的元素,并解锁。
4.2.2 poll() 方法

poll() 方法用于从队列的头部移除并返回元素,如果队列为空,则返回 null。以下是该方法的源码及注释:

java 复制代码
// 从队列的头部移除并返回元素,如果队列为空,则返回 null
public E poll() {
    // 获取锁
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 如果队列为空,直接返回 null
        return (count == 0) ? null : dequeue();
    } finally {
        // 解锁
        lock.unlock();
    }
}
  • 首先,获取锁,并检查队列是否为空。如果队列为空,直接返回 null
  • 如果队列中有元素,调用 dequeue 方法从队列中移除元素,并返回该元素。
  • 最后,解锁。
4.2.3 poll(long timeout, TimeUnit unit) 方法

poll(long timeout, TimeUnit unit) 方法用于从队列的头部移除并返回元素,如果队列为空,则当前线程会等待指定的时间,如果在指定时间内队列中有元素可用,则移除并返回元素,否则返回 null。以下是该方法的源码及注释:

java 复制代码
// 从队列的头部移除并返回元素,如果队列为空,则等待指定的时间
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    // 将超时时间转换为纳秒
    long nanos = unit.toNanos(timeout);
    // 获取锁
    final ReentrantLock lock = this.lock;
    // 允许线程在等待过程中被中断
    lock.lockInterruptibly();
    try {
        // 当队列为空时,进入循环
        while (count == 0) {
            // 如果超时时间已到,返回 null
            if (nanos <= 0)
                return null;
            // 当前线程在 notEmpty 条件上等待指定的时间
            nanos = notEmpty.awaitNanos(nanos);
        }
        // 从队列中移除元素
        return dequeue();
    } finally {
        // 解锁
        lock.unlock();
    }
}
  • 首先,将超时时间转换为纳秒。
  • 获取锁,并在队列为空时,当前线程在 notEmpty 条件上等待指定的时间。
  • 如果在指定时间内队列中有元素可用,调用 dequeue 方法从队列中移除元素,并返回该元素。
  • 如果超时时间已到,队列仍然为空,则返回 null
  • 最后,解锁。

4.3 查看操作

4.3.1 peek() 方法

peek() 方法用于查看队列的头部元素,但不移除该元素,如果队列为空,则返回 null。以下是该方法的源码及注释:

java 复制代码
// 查看队列的头部元素,但不移除该元素,如果队列为空,则返回 null
public E peek() {
    // 获取锁
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 如果队列为空,返回 null,否则返回 takeIndex 所指向的元素
        return itemAt(takeIndex); // null when queue is empty
    } finally {
        // 解锁
        lock.unlock();
    }
}

// 获取指定索引位置的元素的方法
final E itemAt(int i) {
    return (E) items[i];
}
  • 首先,获取锁。
  • 检查队列是否为空,如果为空,返回 null,否则调用 itemAt 方法返回 takeIndex 所指向的元素。
  • 最后,解锁。

4.4 判断队列是否为空(isEmpty)

4.4.1 isEmpty() 方法

isEmpty() 方法用于判断队列是否为空。以下是该方法的源码及注释:

java 复制代码
// 判断队列是否为空
public boolean isEmpty() {
    // 如果队列中元素的数量为 0,返回 true,否则返回 false
    return count == 0;
}
  • 该方法通过检查队列中元素的数量是否为 0 来判断队列是否为空。

4.5 获取队列元素数量(size)

4.5.1 size() 方法

size() 方法用于获取队列中元素的数量。以下是该方法的源码及注释:

java 复制代码
// 获取队列中元素的数量
public int size() {
    // 获取锁
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 返回队列中元素的数量
        return count;
    } finally {
        // 解锁
        lock.unlock();
    }
}
  • 首先,获取锁。
  • 返回队列中元素的数量。
  • 最后,解锁。

五、迭代器的实现

5.1 迭代器接口

ArrayBlockingQueue 类实现了 Iterable 接口,因此可以使用 iterator() 方法获取一个迭代器来遍历队列中的元素。以下是 iterator() 方法的源码及注释:

java 复制代码
// 获取一个迭代器,用于遍历队列中的元素
public Iterator<E> iterator() {
    // 返回一个 Itr 对象
    return new Itr();
}

// 内部类 Itr 实现了 Iterator 接口,用于迭代 ArrayBlockingQueue 中的元素
private class Itr implements Iterator<E> {
    // 下一个要返回的元素的索引
    private int cursor;
    // 最后返回的元素的索引
    private int lastRet;
    // 队列的初始版本号
    private int prevTakeIndex;
    // 队列的初始元素数量
    private int prevCount;

    // 迭代器的构造函数
    Itr() {
        // 获取锁
        final ReentrantLock lock = ArrayBlockingQueue.this.lock;
        lock.lock();
        try {
            // 初始化 prevTakeIndex 为当前的 takeIndex
            prevTakeIndex = takeIndex;
            // 初始化 prevCount 为当前的元素数量
            prevCount = count;
            // 初始化 cursor 为 takeIndex
            cursor = takeIndex;
            // 初始化 lastRet 为 -1
            lastRet = -1;
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 判断是否还有下一个元素
    public boolean hasNext() {
        // 获取锁
        final ReentrantLock lock = ArrayBlockingQueue.this.lock;
        lock.lock();
        try {
            // 调整迭代器的状态
            if (prevTakeIndex != takeIndex)
                adjustCursor();
            // 如果 cursor 不等于 putIndex,说明还有下一个元素,返回 true,否则返回 false
            return cursor != putIndex;
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 获取下一个元素
    public E next() {
        // 获取锁
        final ReentrantLock lock = ArrayBlockingQueue.this.lock;
        lock.lock();
        try {
            // 调整迭代器的状态
            if (prevTakeIndex != takeIndex)
                adjustCursor();
            // 如果 cursor 等于 putIndex,说明没有下一个元素,抛出 NoSuchElementException 异常
            if (cursor == putIndex)
                throw new NoSuchElementException();
            // 获取当前 cursor 所指向的元素
            lastRet = cursor;
            E x = itemAt(cursor);
            // 更新 cursor 的值,如果达到数组末尾,则将其置为 0
            if (++cursor == items.length)
                cursor = 0;
            return x;
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 删除当前迭代的元素
    public void remove() {
        // 如果 lastRet 为 -1,说明还没有调用过 next 方法,抛出 IllegalStateException 异常
        if (lastRet < 0)
            throw new IllegalStateException();
        // 获取锁
        final ReentrantLock lock = ArrayBlockingQueue.this.lock;
        lock.lock();
        try {
            // 调整迭代器的状态
            if (prevTakeIndex != takeIndex)
                adjustCursor();
            // 从队列中移除 lastRet 所指向的元素
            int i = lastRet;
            while (true) {
                int nexti = i + 1;
                if (nexti == items.length)
                    nexti = 0;
                if (nexti == putIndex) {
                    items[i] = null;
                    if (i == takeIndex) {
                        takeIndex = nexti;
                    } else {
                        int s;
                        int n = items.length;
                        for (s = count; s > 0; s--) {
                            int si = dec(i, n);
                            items[i] = items[si];
                            i = si;
                        }
                        if (putIndex == 0)
                            putIndex = n - 1;
                        else
                            putIndex--;
                        count--;
                    }
                    lastRet = -1;
                    prevCount = count;
                    prevTakeIndex = takeIndex;
                    return;
                }
                items[i] = items[nexti];
                i = nexti;
            }
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 调整迭代器状态的方法
    private void adjustCursor() {
        // 如果 prevCount 不等于当前的元素数量
        if (prevCount != count) {
            // 计算元素数量的变化
            int skipped = (prevCount + items.length - count) % items.length;
            // 调整 cursor 的值
            for (int i = 0; i < skipped; i++) {
                if (cursor == putIndex)
                    cursor = takeIndex;
                else if (++cursor == items.length)
                    cursor = 0;
            }
            // 更新 prevCount 和 prevTakeIndex
            prevCount = count;
            prevTakeIndex = takeIndex;
        }
    }

    // 递减索引的方法
    private int dec(int i, int modulus) {
        return ((i - 1 >= 0) ? i - 1 : modulus - 1);
    }
}
  • iterator() 方法返回一个 Itr 对象,用于迭代 ArrayBlockingQueue 中的元素。
  • Itr 类实现了 Iterator 接口,提供了以下方法:
    • hasNext():判断是否还有下一个元素。在判断之前,会调用 adjustCursor 方法调整迭代器的状态。
    • next():获取下一个元素。在获取元素之前,会调用 adjustCursor 方法调整迭代器的状态。如果没有下一个元素,会抛出 NoSuchElementException 异常。
    • remove():删除当前迭代的元素。在删除元素之前,会调用 adjustCursor 方法调整迭代器的状态。如果还没有调用过 next 方法,会抛出 IllegalStateException 异常。

5.2 迭代顺序

ArrayBlockingQueue 的迭代器按照队列中元素的顺序进行迭代,从队列的头部开始,依次向后遍历。例如,以下是一个使用迭代器遍历 ArrayBlockingQueue 的示例代码:

java 复制代码
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;

public class ArrayBlockingQueueIterationExample {
    public static void main(String[] args) {
        // 创建一个容量为 5 的 ArrayBlockingQueue
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
        // 向队列中添加元素
        queue.add(1);
        queue.add(2);
        queue.add(3);

        // 使用迭代器遍历队列中的元素
        Iterator<Integer> iterator = queue.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

在上述示例代码中,首先创建一个容量为 5 的 ArrayBlockingQueue,并向队列中添加元素。然后使用迭代器遍历队列中的元素,迭代器会按照元素的插入顺序依次输出元素。

六、线程安全机制

6.1 锁机制

ArrayBlockingQueue 使用 ReentrantLock 来保证线程安全。所有的入队和出队操作都需要先获取该锁,确保在同一时间只有一个线程可以对队列进行操作。例如,在 put 方法和 take 方法中,都会先调用 lock.lock() 方法获取锁,操作完成后再调用 lock.unlock() 方法释放锁。

java 复制代码
// put 方法中的锁操作
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();
    }
}

// take 方法中的锁操作
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

通过这种锁机制,ArrayBlockingQueue 保证了在多线程环境下对队列的操作是线程安全的,避免了数据不一致的问题。

6.2 条件变量

ArrayBlockingQueue 使用 Condition 对象 notEmptynotFull 来实现线程的阻塞和唤醒操作。当队列为空时,尝试从队列中获取元素的线程会在 notEmpty 条件上等待;当队列已满时,尝试向队列中插入元素的线程会在 notFull 条件上等待。

当有新元素加入队列时,会唤醒一个在 notEmpty 条件上等待的线程;当有元素从队列中移除时,会唤醒一个在 notFull 条件上等待的线程。例如,在 enqueue 方法中,插入元素后会调用 notEmpty.signal() 方法唤醒一个在 notEmpty 条件上等待的线程;在 dequeue 方法中,移除元素后会调用 notFull.signal() 方法唤醒一个在 notFull 条件上等待的线程。

java 复制代码
// enqueue 方法中的唤醒操作
private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}

// dequeue 方法中的唤醒操作
private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

这种机制可以有效地控制线程的执行顺序,避免线程的忙等待,提高系统的性能。

七、性能分析

7.1 时间复杂度分析

  • 插入操作 :在队列的尾部插入元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。因为只需要将元素放入 putIndex 所指向的位置,并更新 putIndex 的值,不需要移动其他元素。
  • 删除操作 :在队列的头部删除元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。只需要从 takeIndex 所指向的位置取出元素,并更新 takeIndex 的值,不需要移动其他元素。
  • 查看操作 :查看队列的头部元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。只需要直接访问 takeIndex 所指向的位置。
  • 迭代操作 :使用迭代器遍历队列中的元素的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是队列中元素的数量。因为需要依次访问队列中的每个元素。

7.2 空间复杂度分析

ArrayBlockingQueue 的空间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 是队列的容量。这是因为需要创建一个长度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 的数组来存储队列中的元素。

7.3 并发性能分析

7.3.1 锁竞争对性能的影响

由于 ArrayBlockingQueue 使用单锁机制,所有的入队和出队操作都需要竞争同一把锁,因此在高并发场景下,锁竞争可能会成为性能瓶颈。当多个线程同时竞争锁时,会导致线程阻塞,从而降低系统的吞吐量。

例如,当多个线程同时尝试入队或出队操作时,只有一个线程可以获取到锁,其他线程需要等待。这种锁竞争会增加线程的上下文切换开销,影响系统的性能。

7.3.2 公平锁和非公平锁的性能差异

ArrayBlockingQueue 的构造函数中可以指定是否使用公平锁。公平锁会按照线程请求锁的顺序来分配锁,而非公平锁则不保证这一点。

公平锁可以保证线程的公平性,但会增加锁竞争的开销,因为需要维护一个线程队列来记录线程的请求顺序。非公平锁在某些情况下可以提高性能,因为它允许线程在锁释放时立即尝试获取锁,而不需要排队等待。

以下是一个简单的示例代码,展示了公平锁和非公平锁的性能差异:

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ArrayBlockingQueueFairnessExample {
    private static final int QUEUE_CAPACITY = 1000;
    private static final int THREAD_COUNT = 10;
    private static final int OPERATIONS_PER_THREAD = 1000;

    public static void main(String[] args) {
        // 测试公平锁
        testQueue(true);
        // 测试非公平锁
        testQueue(false);
    }

    private static void testQueue(boolean fair) {
        ArrayBlockingQueue
java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ArrayBlockingQueueFairnessExample {
    private static final int QUEUE_CAPACITY = 1000;
    private static final int THREAD_COUNT = 10;
    private static final int OPERATIONS_PER_THREAD = 1000;

    public static void main(String[] args) {
        // 测试公平锁
        testQueue(true);
        // 测试非公平锁
        testQueue(false);
    }

    private static void testQueue(boolean fair) {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(QUEUE_CAPACITY, fair);
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);

        long startTime = System.currentTimeMillis();

        // 提交入队任务
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
                    try {
                        queue.put(j);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            });
        }

        // 提交出队任务
        for (int i = 0; i < THREAD_COUNT; i++) {
            executor.submit(() -> {
                for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
                    try {
                        queue.take();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            });
        }

        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("Fairness: " + fair + ", Time taken: " + (endTime - startTime) + " ms");
    }
}

在上述代码中,通过创建多个线程分别执行入队和出队操作,对比了使用公平锁和非公平锁时 ArrayBlockingQueue 的性能表现。通常情况下,非公平锁由于减少了线程排队等待的开销,在高并发场景下会比公平锁具有更好的性能表现,但公平锁能保证线程获取锁的公平性,适用于对公平性有严格要求的场景。

7.4 与其他队列的性能对比

7.4.1 与 LinkedBlockingQueue 的性能对比
  • 插入和删除操作ArrayBlockingQueue 基于数组实现,插入和删除操作在数组的固定位置进行,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),但由于采用单锁机制,在高并发下锁竞争可能导致性能下降。LinkedBlockingQueue 基于链表实现,插入和删除操作同样具有 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 的时间复杂度,并且它采用双锁机制(入队和出队分别使用不同的锁),使得入队和出队操作可以并行进行,在高并发场景下的性能通常优于 ArrayBlockingQueue
  • 空间利用率ArrayBlockingQueue 使用数组存储元素,数组的大小在创建时固定,空间利用率较高,不存在链表节点的额外引用开销。而 LinkedBlockingQueue 每个链表节点都包含元素引用和指向下一个节点的引用,会占用更多的内存空间。
  • 初始化性能ArrayBlockingQueue 在初始化时需要分配固定大小的数组,如果数组较大,初始化时间会相对较长。LinkedBlockingQueue 初始化时只创建链表的头节点,初始化相对较快 。
7.4.2 与 ConcurrentLinkedQueue 的性能对比
  • 阻塞特性ArrayBlockingQueue 是有界阻塞队列,当队列满或空时,相关操作会阻塞线程;而 ConcurrentLinkedQueue 是无界非阻塞队列,入队和出队操作不会阻塞线程,适合对实时性要求高且不需要限制队列大小的场景。
  • 并发性能ConcurrentLinkedQueue 采用无锁算法(如 CAS 操作)实现,避免了锁竞争带来的性能损耗,在高并发场景下具有极高的吞吐量。ArrayBlockingQueue 受单锁机制影响,在高并发时锁竞争可能导致性能瓶颈。
  • 内存占用ConcurrentLinkedQueue 由于是无界队列,在元素不断入队的情况下,如果没有及时出队,可能会导致内存占用持续增加,甚至引发内存溢出。ArrayBlockingQueue 有固定容量,内存占用相对可控。

八、使用场景与示例

8.1 生产者 - 消费者模型

ArrayBlockingQueue 非常适合实现生产者 - 消费者模型,通过它可以有效解耦生产者和消费者线程,并且控制数据的生产和消费速度。

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;

// 生产者线程类
class Producer implements Runnable {
    private final ArrayBlockingQueue<Integer> queue;

    public Producer(ArrayBlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                int data = i;
                System.out.println("Producing: " + data);
                // 将数据放入队列,若队列满则阻塞
                queue.put(data);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

// 消费者线程类
class Consumer implements Runnable {
    private final ArrayBlockingQueue<Integer> queue;

    public Consumer(ArrayBlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                // 从队列中取出数据,若队列为空则阻塞
                int data = queue.take();
                System.out.println("Consuming: " + data);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class ProducerConsumerExample {
    public static void main(String[] args) {
        // 创建一个容量为 3 的 ArrayBlockingQueue
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);

        Thread producerThread = new Thread(new Producer(queue));
        Thread consumerThread = new Thread(new Consumer(queue));

        producerThread.start();
        consumerThread.start();
    }
}

在上述代码中,Producer 线程不断生产数据并放入 ArrayBlockingQueue 中,当队列满时,put 操作会阻塞生产者线程;Consumer 线程从队列中取出数据进行消费,当队列为空时,take 操作会阻塞消费者线程。通过这种方式,实现了生产者和消费者之间的协调与同步。

8.2 任务缓冲与线程池配合

在多线程任务处理系统中,ArrayBlockingQueue 常作为任务缓冲队列与线程池配合使用,控制任务的处理节奏。

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 任务类
class Task implements Runnable {
    private final int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is running");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Task " + taskId + " has finished");
    }
}

public class TaskBufferExample {
    public static void main(String[] args) {
        // 创建一个容量为 5 的 ArrayBlockingQueue 作为任务缓冲队列
        ArrayBlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(5);
        // 创建线程池,线程池大小为 3
        ExecutorService executorService = Executors.newFixedThreadPool(3, r -> {
            Thread thread = new Thread(r);
            thread.setName("TaskExecutor-" + thread.getId());
            return thread;
        });

        for (int i = 0; i < 10; i++) {
            Task task = new Task(i);
            try {
                // 将任务放入队列,若队列满则阻塞
                taskQueue.put(task);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            executorService.execute(taskQueue.poll());
        }

        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

在此示例中,ArrayBlockingQueue 用于存储待执行的任务,线程池从队列中获取任务并执行。当队列满时,新任务的加入会被阻塞,从而控制任务的堆积数量,避免系统资源被过度占用。

8.3 数据缓冲与流处理

在数据处理场景中,ArrayBlockingQueue 可作为数据缓冲区,用于缓存从数据源读取的数据,供后续处理线程进行处理。

java 复制代码
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;

// 数据生成线程类
class DataGenerator implements Runnable {
    private final ArrayBlockingQueue<Integer> queue;

    public DataGenerator(ArrayBlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        Random random = new Random();
        try {
            while (true) {
                int data = random.nextInt(100);
                System.out.println("Generating data: " + data);
                queue.put(data);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

// 数据处理线程类
class DataProcessor implements Runnable {
    private final ArrayBlockingQueue<Integer> queue;

    public DataProcessor(ArrayBlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        try {
            while (true) {
                int data = queue.take();
                System.out.println("Processing data: " + data);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class DataBufferStreamExample {
    public static void main(String[] args) {
        // 创建一个容量为 4 的 ArrayBlockingQueue 作为数据缓冲区
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(4);

        Thread generatorThread = new Thread(new DataGenerator(queue));
        Thread processorThread = new Thread(new DataProcessor(queue));

        generatorThread.start();
        processorThread.start();
    }
}

上述代码中,DataGenerator 线程模拟数据源生成数据并放入 ArrayBlockingQueue 中,DataProcessor 线程从队列中取出数据进行处理。通过队列的阻塞特性,实现了数据生成和处理速度的平衡。

九、常见问题与解决方案

9.1 队列满导致的阻塞问题

9.1.1 问题描述

ArrayBlockingQueue 达到其指定容量后,后续的入队操作(如 put 方法)会导致线程阻塞。在一些高并发或长时间运行的系统中,如果生产者线程的生产速度远大于消费者线程的消费速度,队列会迅速被填满,进而使得大量生产者线程处于阻塞状态,影响系统的整体性能甚至导致系统响应缓慢。

9.1.2 解决方案
  • 调整队列容量:根据实际业务场景,合理评估生产者和消费者的速度差异,适当增大队列容量。例如,如果预计生产者的生产速度在峰值时为每秒 100 个数据,消费者的处理速度为每秒 50 个数据,并且允许一定的缓冲时间,可以将队列容量设置为能容纳一定时间内生产的数据量 。但需注意,过大的容量可能会占用过多内存。
  • 使用非阻塞的入队方法 :使用 offer 方法替代 put 方法。offer 方法在队列满时会立即返回 false,而不会阻塞线程。生产者线程可以根据返回结果进行相应处理,如记录日志、等待一段时间后重试或者丢弃数据(在允许数据丢失的场景下)。
java 复制代码
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
int data = 1;
if (!queue.offer(data)) {
    System.out.println("Queue is full, data " + data + " cannot be added");
}
  • 增加消费者线程:通过增加消费者线程的数量,提高数据的消费速度,避免队列长时间处于满的状态。可以结合线程池来管理消费者线程,根据队列的状态动态调整线程池中的线程数量 。

9.2 队列为空导致的阻塞问题

9.2.1 问题描述

ArrayBlockingQueue 为空时,执行出队操作(如 take 方法)的线程会被阻塞。在某些情况下,如果消费者线程执行出队操作过于频繁,而生产者线程生产数据的速度较慢,会导致大量消费者线程处于阻塞状态,使得系统资源闲置,降低系统的运行效率。

9.2.2 解决方案
  • 使用非阻塞的出队方法 :使用 poll 方法替代 take 方法。poll 方法在队列为空时会立即返回 null,不会阻塞线程。消费者线程可以根据返回结果进行处理,如等待一段时间后再次尝试获取数据或者执行其他备用任务。
java 复制代码
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
Integer data = queue.poll();
if (data == null) {
    System.out.println("Queue is empty, no data to consume");
}
  • 设置超时时间 :使用 poll(long timeout, TimeUnit unit) 方法,该方法在队列为空时,会等待指定的时间。如果在超时时间内队列中有数据,则返回数据;否则返回 null。通过设置合理的超时时间,可以避免消费者线程长时间阻塞。
java 复制代码
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
try {
    Integer data = queue.poll(2, TimeUnit.SECONDS);
    if (data == null) {
        System.out.println("Timeout, no data available in 2 seconds");
    } else {
        System.out.println("Retrieved data: " + data);
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

9.3 迭代器使用中的并发修改问题

9.3.1 问题描述

在使用 ArrayBlockingQueue 的迭代器进行遍历的过程中,如果其他线程同时对队列进行修改操作(如入队或出队),会导致 ConcurrentModificationException 异常。这是因为迭代器在遍历过程中会检查队列的状态是否发生变化,如果发生变化则抛出异常。

9.3.2 解决方案
  • 使用同步机制 :在对队列进行遍历和修改操作时,使用同步代码块或锁机制,确保在同一时间只有一个线程可以对队列进行操作。例如,使用 synchronized 关键字对相关代码进行同步。
java 复制代码
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
synchronized (queue) {
    // 遍历队列
    for (Integer element : queue) {
        // 处理元素
    }
    // 进行修改操作
    queue.put(1);
}
  • 克隆队列后再遍历:在需要遍历队列时,先对队列进行克隆,然后在克隆的队列上进行遍历操作。这样即使原队列在遍历过程中被修改,也不会影响克隆队列的遍历。但需注意,克隆操作可能会带来一定的性能开销和内存占用。
java 复制代码
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
// 克隆队列
ArrayBlockingQueue<Integer> clonedQueue = new ArrayBlockingQueue<>(queue);
for (Integer element : clonedQueue) {
    // 处理元素
}

十、总结与展望

10.1 总结

ArrayBlockingQueue 作为 Java 并发包中基于数组实现的有界阻塞队列,具备诸多特性和优势:

  • 线程安全 :通过 ReentrantLockCondition 条件变量,保证了多线程环境下对队列操作的原子性和线程安全,避免了数据不一致问题。无论是入队、出队还是查看操作,都能在锁的保护下有序进行,同时利用条件变量实现线程的阻塞与唤醒,高效地协调线程间的执行顺序。
  • 有界特性:在创建时需指定容量,这使得它适用于对资源占用有明确限制的场景,能够有效控制内存使用,防止队列无限增长导致内存溢出 。通过合理设置容量,可以平衡生产者和消费者之间的数据处理速度。
  • 高效的基本操作:插入和删除操作在数组固定位置进行,时间复杂度为 $O
相关推荐
牛马baby15 分钟前
Java高频面试之并发编程-11
java·开发语言·面试
移动开发者1号28 分钟前
Android现代进度条替代方案
android·app
万户猴28 分钟前
【Android蓝牙开发实战-11】蓝牙BLE多连接机制全解析1
android·蓝牙
RichardLai8831 分钟前
[Flutter 基础] - Flutter基础组件 - Icon
android·flutter
前行的小黑炭38 分钟前
Android LiveData源码分析:为什么他刷新数据比Handler好,能更节省资源,解决内存泄漏的隐患;
android·kotlin·android jetpack
我是哪吒43 分钟前
分布式微服务系统架构第124集:架构
后端·面试·github
Jenlybein1 小时前
进阶学习 Javascript ? 来看看这篇系统复习笔记 [ 面向对象篇 ]
前端·javascript·面试
清霜之辰1 小时前
安卓 Compose 相对传统 View 的优势
android·内存·性能·compose
_祝你今天愉快1 小时前
再看!NDK交叉编译动态库并在Android中调用
android
Jenlybein1 小时前
进阶学习 Javascript ? 来看看这篇系统复习笔记 [ Generator 篇 ]
前端·javascript·面试