深度剖析 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
相关推荐
硬件学长森哥1 小时前
Android音视频多媒体开源框架基础大全
android·图像处理·音视频
二流小码农2 小时前
鸿蒙开发:CodeGenie万能卡片生成
android·ios·harmonyos
爱笑的眼睛112 小时前
HarmonyOS 组件复用面试宝典 [特殊字符]
华为·面试·harmonyos·harmonyos next
章鱼paul帝2 小时前
浅谈 iOS 字典 NSDictionary 的底层实现原理
面试
没有了遇见2 小时前
Android 直播间动画动画队列实现
android
月山知了2 小时前
Android有的命令不需要root权限,有的命令需要root权限是如何实现的
android
寒山李白3 小时前
Java 传输较大数据的相关问题解析和面试问答
java·开发语言·面试·传输
科技道人3 小时前
Android 实体键盘 设置默认布局
android·实体键盘·设置默认键盘语言
SHUIPING_YANG3 小时前
tp3.1临时连接指定数据库,切片分类in查询,带过滤需要的数据
android·数据库