深度剖析 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 中,存在多种队列实现,如 LinkedBlockingQueue
、PriorityBlockingQueue
等,它们与 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
类型的锁,用于保证在多线程环境下对队列的操作是线程安全的。所有的入队和出队操作都需要先获取该锁。notEmpty
和notFull
:分别是与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();
}
}
这些构造函数提供了多种方式来创建 ArrayBlockingQueue
。ArrayBlockingQueue(int capacity)
构造函数创建一个指定容量的阻塞队列,使用非公平锁;ArrayBlockingQueue(int capacity, boolean fair)
构造函数创建一个指定容量和是否公平的阻塞队列;ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c)
构造函数创建一个包含指定集合元素的阻塞队列,容量为集合的大小。
3.3 数组结构
ArrayBlockingQueue
使用数组来存储队列中的元素,通过 takeIndex
和 putIndex
两个索引来管理数组的头部和尾部。当有新元素加入队列时,会将元素放入 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
对象 notEmpty
和 notFull
来实现线程的阻塞和唤醒操作。当队列为空时,尝试从队列中获取元素的线程会在 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 并发包中基于数组实现的有界阻塞队列,具备诸多特性和优势:
- 线程安全 :通过
ReentrantLock
和Condition
条件变量,保证了多线程环境下对队列操作的原子性和线程安全,避免了数据不一致问题。无论是入队、出队还是查看操作,都能在锁的保护下有序进行,同时利用条件变量实现线程的阻塞与唤醒,高效地协调线程间的执行顺序。 - 有界特性:在创建时需指定容量,这使得它适用于对资源占用有明确限制的场景,能够有效控制内存使用,防止队列无限增长导致内存溢出 。通过合理设置容量,可以平衡生产者和消费者之间的数据处理速度。
- 高效的基本操作:插入和删除操作在数组固定位置进行,时间复杂度为 $O