概述
Queue(队列)是 Java 集合框架中承上启下的关键分支,它以“先进先出”为基本契约,却在工程实践中衍生出丰富多变的实现形态。从单机内存中的简单双端队列 ArrayDeque,到支撑线程池任务调度的 BlockingQueue 家族,再到基于无锁 CAS 的高性能并发队列 ConcurrentLinkedQueue,Queue 接口的每一个实现类都蕴含着精巧的数据结构与并发控制哲学。本文是 Java 集合框架系列第四篇,将站在专家视角,从接口设计演进、存储底层内存布局、并发原语(synchronized / ReentrantLock / CAS / volatile)、阻塞唤醒机制(Condition / LockSupport)到性能瓶颈与实战陷阱,系统化剖析 Queue 分支的全貌。文中所有源码分析均基于 JDK 8,并配合详细的流程图与时序图,带你理解每一个插入、删除、查询操作背后的真实逻辑。阅读本文后,你将不仅掌握各类队列的使用方式,更能深入理解其内部工作原理,具备在复杂场景下精准选型与调优的能力。
模块 1:Queue 接口设计——先进先出的队列契约
java.util.Queue 接口在 Java 5 引入,继承自 Collection,它约定了队列这一数据结构的核心操作语义。Queue 接口最精妙的设计在于提供两套功能等价但异常处理策略不同的方法:
| 操作类型 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入 | add(e) | offer(e) |
| 移除 | remove() | poll() |
| 检查 | element() | peek() |
- 抛异常型方法:适用于确信操作必定成功的场景(如队列无界或容量充足),当操作违反容量限制时抛出
IllegalStateException或NoSuchElementException。 - 返回特殊值型方法:
offer在容量不足时返回false,poll和peek在队列为空时返回null。这种设计允许调用方以更灵活、非中断的方式处理边界条件,尤其适合有界队列和阻塞场景的降级处理。
随着并发编程需求的增长,Java 5 进一步引入了 BlockingQueue 接口,它继承自 Queue,增加了阻塞语义的方法:put(e)、take()、带超时的 offer(e, time, unit) 和 poll(time, unit),以及批量操作 drainTo(Collection)。BlockingQueue 成为生产者-消费者模型的基石。
Deque(双端队列)接口同时继承 Queue,支持在头尾两端进行插入、移除和检查操作,同样提供抛异常和返回特殊值两套方法,其实现类(如 ArrayDeque 和 LinkedList)既可以作为 FIFO 队列,也可以作为 LIFO 栈使用。
TransferQueue 是 Java 7 引入的更细粒度的阻塞队列接口,它扩展了 BlockingQueue,增加了 transfer(e) 和 tryTransfer(e) 方法,要求生产者必须等待消费者直接取走元素,实现了“零容量”的直接交付语义。
下面的 Mermaid 类图展示了 Queue 体系的核心接口继承层次及主要实现类的实现关系:
classDiagram
class Iterable {
<<interface>>
}
class Collection {
<<interface>>
}
class Queue {
<<interface>>
+add(E e) boolean
+offer(E e) boolean
+remove() E
+poll() E
+element() E
+peek() E
}
class Deque {
<<interface>>
+addFirst(E e)
+addLast(E e)
+offerFirst(E e)
+offerLast(E e)
+removeFirst()
+removeLast()
+pollFirst()
+pollLast()
+getFirst()
+getLast()
+peekFirst()
+peekLast()
}
class BlockingQueue {
<<interface>>
+put(E e)
+take() E
+offer(E e long timeout TimeUnit unit)
+poll(long timeout TimeUnit unit)
+drainTo(Collection~E~ c)
}
class TransferQueue {
<<interface>>
+transfer(E e)
+tryTransfer(E e)
+tryTransfer(E e long timeout TimeUnit unit)
+hasWaitingConsumer()
}
Iterable <|-- Collection
Collection <|-- Queue
Queue <|-- Deque
Queue <|-- BlockingQueue
BlockingQueue <|-- TransferQueue
class ArrayDeque
class LinkedList
class PriorityQueue
class ArrayBlockingQueue
class LinkedBlockingQueue
class PriorityBlockingQueue
class DelayQueue
class SynchronousQueue
class LinkedTransferQueue
class ConcurrentLinkedQueue
Deque <|.. ArrayDeque
Deque <|.. LinkedList
Queue <|.. PriorityQueue
BlockingQueue <|.. ArrayBlockingQueue
BlockingQueue <|.. LinkedBlockingQueue
BlockingQueue <|.. PriorityBlockingQueue
BlockingQueue <|.. DelayQueue
BlockingQueue <|.. SynchronousQueue
TransferQueue <|.. LinkedTransferQueue
Queue <|.. ConcurrentLinkedQueue
图表说明:上述类图清晰地描绘了 Queue 接口的继承层次。Queue 作为顶层抽象,向下派生出 Deque(双端队列)、BlockingQueue(阻塞队列)和 TransferQueue(直接交付队列)。每个接口引入了新的行为契约:Deque 增加头尾双向操作,BlockingQueue 增加阻塞等待语义,TransferQueue 增加一对一交付语义。图中展示了各核心实现类对接口的落实关系,例如 ArrayDeque 和 LinkedList 实现了 Deque,ArrayBlockingQueue 和 LinkedBlockingQueue 实现了 BlockingQueue,ConcurrentLinkedQueue 直接实现 Queue 但不阻塞。
模块 2:ArrayDeque 深度剖析——基于循环数组的高效双端队列
ArrayDeque 是 JDK 1.6 引入的基于循环数组的双端队列实现,它既是高性能栈(Stack),也是高性能 FIFO 队列。在绝大多数场景下,ArrayDeque 的性能均优于传统的 Stack(基于 Vector)和 LinkedList。
Demo 代码(JDK 8 可运行)
import java.util.ArrayDeque;
import java.util.Deque;
public class ArrayDequeDemo {
public static void main(String[] args) {
// 1. 作为栈使用 (LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("First");
stack.push("Second");
stack.push("Third");
System.out.println("Stack pop: " + stack.pop()); // Third
System.out.println("Stack peek: " + stack.peek()); // Second
// 2. 作为队列使用 (FIFO)
Deque<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println("Queue poll: " + queue.poll()); // A
System.out.println("Queue peek: " + queue.peek()); // B
// 3. 作为双端队列使用
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.addFirst(0);
System.out.println("Deque: " + deque); // [0, 1, 2]
System.out.println("Remove first: " + deque.removeFirst()); // 0
System.out.println("Remove last: " + deque.removeLast()); // 2
}
}
底层原理深入剖析
存储结构
ArrayDeque 内部使用 Object[] elements 数组存储元素,并通过两个关键索引变量 head 和 tail 构成逻辑上的循环数组。head 始终指向队列头部第一个有效元素的索引,tail 指向队列尾部下一个可插入位置的索引。这种设计使得头尾插入/删除操作仅需 O(1) 时间。
classDiagram
class ArrayDeque {
-Object[] elements
-int head
-int tail
+addFirst(E e)
+addLast(E e)
+pollFirst()
+pollLast()
-doubleCapacity()
}
图表说明:ArrayDeque 的核心成员包括 elements 数组、head 头部指针和 tail 尾部指针。所有双端操作均围绕这三个成员展开。当 head 和 tail 相遇时,表示队列已满,此时触发 doubleCapacity() 扩容。
插入操作:addFirst 与 addLast 源码级流程
addFirst 方法将元素插入头部,其核心操作为:计算新头下标,放入元素,更新 head,必要时扩容。JDK 8 源码实现如下(简化关键逻辑):
public void addFirst(E e) {
if (e == null) throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail) doubleCapacity();
}
- 第一步:将
head减 1,并与(elements.length - 1)按位与,实现循环取模,得到新头部索引。 - 第二步:将元素存入新索引位置。
- 第三步:若
head == tail,说明插入前队列已满,触发扩容。
addLast 方法对称地在尾部插入,元素放入 tail 位置,然后 tail = (tail + 1) & (elements.length - 1),同样在满时扩容。
public void addLast(E e) {
if (e == null) throw new NullPointerException();
elements[tail] = e;
if ((tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity();
}
删除操作:pollFirst 与 pollLast 源码级流程
pollFirst 移除头部元素:取出 head 位置元素,置 null,然后 head = (head + 1) & (elements.length - 1)。pollLast 则先将 tail 减 1 得到待移除元素的索引,取出后置 null,更新 tail。
public E pollFirst() {
int h = head;
E result = (E) elements[h];
if (result == null) return null;
elements[h] = null;
head = (h + 1) & (elements.length - 1);
return result;
}
扩容机制:doubleCapacity 的循环数组展开
当队列满时,doubleCapacity 创建新数组,容量为原数组两倍,并将循环数组中的元素按顺序复制到新数组的起始位置。源码如下:
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // head 右侧的元素个数
int newCapacity = n << 1; // 容量翻倍
if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
System.arraycopy(elements, p, a, 0, r); // 复制 head 到数组末尾的部分
System.arraycopy(elements, 0, a, r, p); // 复制 0 到 head 的部分
elements = a;
head = 0;
tail = n;
}
该过程将“弯曲”的循环数组“拉直”为线性数组,使 head 归零,tail 指向原长度位置(即旧容量大小)。
查询操作 peekFirst/peekLast
peekFirst 直接返回 elements[head],peekLast 返回 elements[(tail - 1) & (elements.length - 1)],均为 O(1) 且不修改队列。
下面通过流程图综合展示 addFirst、addLast、pollFirst 和扩容的决策过程:
flowchart TD
A["启动双端操作"] --> B{"操作类型"}
B -->|"addFirst"| C["head = (head - 1) & mask"]
C --> D["存储元素到 elements[head]"]
D --> E{"head == tail?"}
E -->|"是"| F["doubleCapacity 扩容并复制"]
F --> G["重设 head=0, tail=旧容量"]
E -->|"否"| H["完成"]
B -->|"addLast"| I["存储元素到 elements[tail]"]
I --> J["tail = (tail + 1) & mask"]
J --> K{"tail == head?"}
K -->|"是"| F
K -->|"否"| H
B -->|"pollFirst"| L["取 elements[head]"]
L --> M{"值非 null?"}
M -->|"是"| N["elements[head]=null, head = (head+1) & mask"]
M -->|"否"| O["返回 null"]
B -->|"pollLast"| P["tail2 = (tail - 1) & mask"]
P --> Q["取 elements[tail2]"]
Q --> M
图表说明:该图展示了 ArrayDeque 核心操作的流程。插入时通过位掩码实现指针循环移动,并在 head==tail 时调用 doubleCapacity;删除时通过检测 null 判断空队列,然后再移动指针。扩容时将循环数组按序复制到线性新数组,使得 head 回到 0。
性能分析
- 时间复杂度:头尾插入/删除操作均为 O(1),无链表节点分配开销。
- 空间消耗:紧凑的循环数组存储,避免了链表节点的额外对象头和指针开销,内存效率高。
- CPU 缓存友好:连续内存布局使得 CPU 缓存行命中率远高于链表结构,迭代遍历性能极佳。
- 扩容开销:扩容需复制全部元素,但均摊复杂度仍为 O(1),且 2 倍扩容策略使得扩容频率较低。
注意事项
- 不支持
null元素:插入null会抛出NullPointerException,这是有意设计,因为poll和peek依靠null表示队列为空。 - 非线程安全:多线程并发修改需外部同步,可考虑使用
Collections.synchronizedDeque或并发队列替代。 - 比
LinkedList更快:LinkedList作为队列使用时,每次插入都需创建Node对象,而ArrayDeque仅操作数组,内存分配压力和 GC 压力更小。
模块 3:PriorityQueue 深度剖析——基于二叉堆的优先级队列
PriorityQueue 是一个基于优先级堆的无界优先级队列,它不遵循严格的 FIFO 顺序,而是按照元素的自然顺序或提供的 Comparator 进行排序。堆顶始终是当前最小(或最大,取决于比较器)的元素。
Demo 代码(JDK 8 可运行)
import java.util.Comparator;
import java.util.PriorityQueue;
public class PriorityQueueDemo {
public static void main(String[] args) {
// 1. 自然排序 (元素必须实现 Comparable)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(1);
pq.offer(3);
pq.offer(2);
System.out.print("PriorityQueue 按优先级出队: ");
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " "); // 1 2 3 5
}
System.out.println();
// 2. 自定义 Comparator (大顶堆)
PriorityQueue<String> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer("Apple");
maxHeap.offer("Banana");
maxHeap.offer("Cherry");
System.out.println("最大元素: " + maxHeap.peek()); // Cherry
// 3. 任务调度场景 (按优先级执行)
class Task implements Comparable<Task> {
String name;
int priority;
Task(String name, int priority) { this.name = name; this.priority = priority; }
public int compareTo(Task o) { return Integer.compare(this.priority, o.priority); }
public String toString() { return name + "(" + priority + ")"; }
}
PriorityQueue<Task> tasks = new PriorityQueue<>();
tasks.offer(new Task("Write Code", 2));
tasks.offer(new Task("Fix Bug", 1));
tasks.offer(new Task("Review PR", 3));
System.out.print("任务执行顺序: ");
while (!tasks.isEmpty()) {
System.out.print(tasks.poll() + " "); // Fix Bug(1) Write Code(2) Review PR(3)
}
}
}
底层原理深入剖析
存储结构
PriorityQueue 内部使用 Object[] queue 数组存储二叉堆。堆是一棵完全二叉树,利用数组的紧凑特性,通过下标关系维护父子节点:
- 父节点下标:
(i - 1) >>> 1 - 左子节点下标:
(i << 1) + 1 - 右子节点下标:
(i << 1) + 2
默认容量为 11,当容量不足时触发扩容。
入队 offer 与上浮 siftUp 源码细节
JDK 8 的 offer 方法:
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length) grow(i + 1);
size = i + 1;
if (i == 0) queue[0] = e;
else siftUp(i, e);
return true;
}
siftUp 根据是否有 Comparator 选择 siftUpComparable 或 siftUpUsingComparator。以自然排序为例:
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
上浮算法将新元素从末尾索引 k 开始,反复与父节点比较,若小于父节点则交换,直到满足堆序。
出队 poll 与下沉 siftDown 源码细节
public E poll() {
if (size == 0) return null;
int s = --size;
modCount++;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0) siftDown(0, x);
return result;
}
siftDown 从堆顶开始,将末尾元素 x 下移:
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
int half = size >>> 1; // 非叶子节点边界
while (k < half) {
int child = (k << 1) + 1;
Object c = queue[child];
int right = child + 1;
if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
c = queue[child = right];
if (key.compareTo((E) c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
下沉时,选择左右子节点中较小者,若 key 大于该子节点,则子节点上移,k 移向该子节点,直至 key 小于等于子节点。
扩容机制
- 当旧容量小于 64 时,扩容为
oldCapacity + oldCapacity + 2(即翻倍加 2)。 - 当旧容量大于等于 64 时,扩容为
oldCapacity + (oldCapacity >> 1)(即 50% 增长)。
下面通过流程图详细描绘 offer 上浮与 poll 下沉过程:
flowchart TD
subgraph Offer["入队 offer"]
O1["新元素 e 放入数组 size 位置 size++"]
O2{"size == 1?"}
O2 -->|"是"| O3["堆顶直接为 e"]
O2 -->|"否"| O4["调用 siftUp k=size-1 x=e"]
O4 --> O5{"k > 0?"}
O5 -->|"是"| O6["parent = (k-1) >>> 1"]
O6 --> O7{"x >= parent?"}
O7 -->|"是"| O8["break"]
O7 -->|"否"| O9["queue[k] = parent k = parent"]
O9 --> O5
O5 -->|"否"| O10["queue[k] = x"]
end
subgraph Poll["出队 poll"]
P1["取出堆顶 result = queue[0]"]
P2["取末尾元素 x = queue[--size]"]
P3["queue[size] = null"]
P4{"size == 0?"}
P4 -->|"是"| P5["返回 result"]
P4 -->|"否"| P6["调用 siftDown k=0 x"]
P6 --> P7{"k < half?"}
P7 -->|"是"| P8["child = 2*k+1 right = child+1"]
P8 --> P9{"右子存在且右子 < 左子?"}
P9 -->|"是"| P10["child = right"]
P9 -->|"否"| P11["保持 child"]
P10 --> P12{"x <= child?"}
P11 --> P12
P12 -->|"是"| P13["break"]
P12 -->|"否"| P14["queue[k] = child k = child"]
P14 --> P7
P7 -->|"否"| P15["queue[k] = x"]
end
图表说明:offer 将新元素追加到数组末尾,通过上浮恢复堆性质;poll 移除堆顶后用末尾元素填补,通过下沉恢复堆性质。下沉时每次选取较小的子节点进行比较,确保堆序不被破坏。父子节点计算完全基于数组下标关系。
性能分析
- 时间复杂度:入队
offer和出队poll均为 O(log n),peek为 O(1),remove(Object)为 O(n)(需线性扫描)。批量建堆heapify为 O(n)。 - 空间消耗:数组存储,无额外节点对象开销,内存紧凑。
- 线程安全:非线程安全,并发修改将导致堆结构破坏或
ConcurrentModificationException。
注意事项
- 迭代顺序不保证优先级:
iterator()返回的迭代器按数组顺序遍历,并非优先级顺序。要按优先级遍历必须反复调用poll()清空队列。 - 元素必须可比较:若未提供
Comparator,元素必须实现Comparable,否则插入时报ClassCastException。 - 不支持
null元素:与ArrayDeque相同,null用于表示特殊语义(poll返回null表示空),因此禁止插入null。
模块 4:BlockingQueue 体系与生产者-消费者模型
BlockingQueue 是 Java 并发包(java.util.concurrent)的核心接口之一,它扩展了 Queue 接口,增加了阻塞等待和超时机制,专为生产者-消费者模式设计。
阻塞语义方法:
put(E e):将元素插入队列,若队列已满则阻塞等待空间可用。take():移除并返回队列头部元素,若队列为空则阻塞等待元素可用。offer(E e, long timeout, TimeUnit unit):在指定时间内尝试插入,超时返回false。poll(long timeout, TimeUnit unit):在指定时间内等待并移除头部元素,超时返回null。drainTo(Collection<? super E> c):一次性将队列中所有可用元素排入给定集合,减少多次加锁开销。
BlockingQueue 有多种实现,各有不同的存储结构、并发控制策略和容量限制,为不同场景提供了丰富的选择。
classDiagram
class BlockingQueue {
<<interface>>
}
class ArrayBlockingQueue {
-Object[] items
-int count
-ReentrantLock lock
-Condition notEmpty
-Condition notFull
}
class LinkedBlockingQueue {
-Node head
-Node last
-int count
-ReentrantLock takeLock
-Condition notEmpty
-ReentrantLock putLock
-Condition notFull
}
class PriorityBlockingQueue {
-Object[] queue
-int size
-ReentrantLock lock
-Condition notEmpty
}
class DelayQueue {
-PriorityQueue q
-Thread leader
-Condition available
}
class SynchronousQueue {
-Transferer transferer
}
class LinkedTransferQueue {
-Node head
-Node tail
}
BlockingQueue <|.. ArrayBlockingQueue
BlockingQueue <|.. LinkedBlockingQueue
BlockingQueue <|.. PriorityBlockingQueue
BlockingQueue <|.. DelayQueue
BlockingQueue <|.. SynchronousQueue
BlockingQueue <|.. LinkedTransferQueue
图表说明:该图展示了 BlockingQueue 的主要实现类及其核心字段。ArrayBlockingQueue 采用单锁双条件,LinkedBlockingQueue 采用双锁双条件,PriorityBlockingQueue 和 DelayQueue 使用单锁,SynchronousQueue 和 LinkedTransferQueue 则采用更复杂的 CAS 配对机制。不同设计决定了它们在并发度、内存占用和阻塞行为上的差异。
模块 5:ArrayBlockingQueue 深度剖析——有界数组阻塞队列
ArrayBlockingQueue 是一个基于数组的有界阻塞队列,它在构造时必须指定固定容量。内部使用一把 ReentrantLock 保护所有访问,并通过两个 Condition(notEmpty 和 notFull)实现精确的线程阻塞与唤醒。
Demo 代码(JDK 8 可运行)
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ArrayBlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
// 生产者线程
Thread producer = new Thread(() -> {
String[] items = {"A", "B", "C", "D"};
try {
for (String item : items) {
System.out.println("Producing: " + item);
queue.put(item); // 队列满时阻塞
System.out.println("Produced: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 4; i++) {
Thread.sleep(1000); // 模拟慢消费
String item = queue.take(); // 队列空时阻塞
System.out.println("Consumed: " + item);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
底层原理深入剖析
存储结构
ArrayBlockingQueue 使用定长数组 Object[] items 存储元素,同时维护 putIndex(下一个插入位置)、takeIndex(下一个移除位置)和 count(当前元素数量)。与 ArrayDeque 类似,通过循环数组逻辑实现高效的头尾操作。
并发控制
核心并发控制由单一 ReentrantLock lock 提供,所有入队、出队方法均需获取该锁。尽管单锁简化了实现,但同时也限制了并发度——同一时刻只能有一个生产者或消费者执行操作。
put 与 take 源码流程解析
put 方法的完整流程(JDK 8):
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();
}
}
enqueue 将元素放入 items[putIndex],然后 putIndex = (putIndex + 1) % items.length,count++,最后调用 notEmpty.signal() 唤醒等待的消费者。
take 方法的对称实现:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) // 队列空时循环等待
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
dequeue 从 items[takeIndex] 取出元素,置 null,takeIndex = (takeIndex + 1) % items.length,count--,并调用 notFull.signal() 唤醒等待的生产者。
阻塞与唤醒机制
Condition 的 await() 方法会原子地释放锁并阻塞当前线程,线程进入条件队列;当被 signal() 唤醒后,线程将从条件队列转移到 AQS 同步队列,重新竞争锁,成功后从 await() 返回继续执行。这种机制精确控制了生产者和消费者的唤醒,避免了无效唤醒。
公平性
ArrayBlockingQueue 提供可选公平参数。公平模式下,锁使用公平策略(new ReentrantLock(true)),等待最久的线程优先获取锁;非公平模式下吞吐量更高,但可能导致线程饥饿。
下面的时序图详细描绘了生产者阻塞、唤醒及锁转移的完整过程,反映 ReentrantLock 与 Condition 的协作:
sequenceDiagram
participant P1 as 生产者P1
participant Lock as ReentrantLock
participant CondFull as notFull条件队列
participant CondEmpty as notEmpty条件队列
participant C1 as 消费者C1
participant Q as ArrayBlockingQueue
P1->>Lock: lockInterruptibly() 获取锁
Lock-->>P1: 获取成功
P1->>Q: 检查 count==容量? (满)
Q-->>P1: 满
P1->>CondFull: notFull.await()
Note over P1: 释放锁,线程进入 CondFull 等待
Lock-->>CondFull: 释放锁
C1->>Lock: lockInterruptibly() 获取锁
Lock-->>C1: 获取成功
C1->>Q: dequeue()
Q-->>C1: 返回元素
C1->>CondFull: notFull.signal()
Note over P1: 从 CondFull 移到 AQS 同步队列
C1->>Lock: unlock()
Lock-->>P1: 唤醒 P1 竞争锁
P1->>Lock: 获取锁成功
P1->>Q: enqueue()
P1->>CondEmpty: notEmpty.signal()
P1->>Lock: unlock()
图表说明:生产者 P1 因队列满进入 notFull 条件等待并释放锁,消费者 C1 获取锁、取出元素后执行 notFull.signal() 将 P1 从条件队列移入同步队列。C1 释放锁后,P1 重新获取锁完成入队,并通过 notEmpty.signal() 唤醒可能的等待消费者。整个流程体现了精确等待与唤醒,避免了必须唤醒所有等待者的“惊群”效应。
性能分析
- 时间复杂度:入队/出队均为 O(1)。
- 并发吞吐:单锁设计导致并发度受限,适合生产者-消费者数量较少、操作频率不极端的场景。当线程数较多时,锁竞争加剧,吞吐量可能下降。
- 内存占用:数组固定大小,无动态扩容开销,内存可预测且 GC 友好。
注意事项
- 容量固定:构造后容量不可变,无法动态扩容,需合理预估队列长度。
- 单锁瓶颈:当生产者和消费者线程数量较多时,锁竞争可能成为性能瓶颈,此时可考虑
LinkedBlockingQueue。 - 公平性代价:启用公平锁会显著降低吞吐量,仅在严格需要防止饥饿的场景下使用。
模块 6:LinkedBlockingQueue 深度剖析——可选有界链表阻塞队列
LinkedBlockingQueue 是基于单向链表的阻塞队列,可选有界(默认 Integer.MAX_VALUE,近乎无界)。其最大的亮点在于采用“双锁”设计——入队锁 putLock 和出队锁 takeLock 分离,使得生产者和消费者在大多数情况下可以并行操作,极大提升并发吞吐量。
Demo 代码(JDK 8 可运行)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class LinkedBlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
// 指定容量,防止无界内存溢出
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(1000);
// 模拟高并发生产者
Runnable producer = () -> {
for (int i = 0; i < 100; i++) {
try {
queue.put(i);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
// 模拟高并发消费者
Runnable consumer = () -> {
for (int i = 0; i < 100; i++) {
try {
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
long start = System.nanoTime();
Thread[] producers = new Thread[10];
Thread[] consumers = new Thread[10];
for (int i = 0; i < 10; i++) {
producers[i] = new Thread(producer);
consumers[i] = new Thread(consumer);
producers[i].start();
consumers[i].start();
}
for (int i = 0; i < 10; i++) {
producers[i].join();
consumers[i].join();
}
long end = System.nanoTime();
System.out.println("LinkedBlockingQueue 耗时: " +
TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
}
}
底层原理深入剖析
存储结构
内部维护一个单向链表,节点为静态内部类 Node,包含 item 和 next 引用。head 指向一个哑元节点(item == null),last 指向尾节点。链表结构使得入队仅需操作尾节点,出队仅需操作头节点,二者互不干扰。
并发控制:双锁分离
takeLock:保护出队操作(take、poll),对应的条件队列为notEmpty。putLock:保护入队操作(put、offer),对应的条件队列为notFull。
由于入队只修改 last 及其后继,出队只修改 head,且链表节点之间的连接使得两个操作在 count > 0 且未满时完全解耦,因此生产者和消费者可以同时进行。
put 与 take 源码级流程及级联唤醒
put 方法源码(简化):
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal(); // 级联唤醒其他生产者
} finally {
putLock.unlock();
}
if (c == 0) // 由空→非空,唤醒消费者
signalNotEmpty();
}
take 方法源码(简化):
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal(); // 级联唤醒其他消费者
} finally {
takeLock.unlock();
}
if (c == capacity) // 由满→非满,唤醒生产者
signalNotFull();
return x;
}
关键点:
- 使用
AtomicInteger count在双锁间传递状态,避免获取对方锁。 - 级联唤醒:当队列仍不满时,当前生产者会额外唤醒另一个等待的生产者,加速处理;消费者端同理。
- 跨工能唤醒仅在边界条件触发(空→非空、满→非满),减少不必要的锁获取。
下面的时序图描绘了双锁并发入队出队及条件唤醒的交互:
sequenceDiagram
participant P1 as 生产者P1
participant PutLock as putLock
participant Queue as LinkedBlockingQueue
participant TakeLock as takeLock
participant C1 as 消费者C1
P1->>PutLock: lock() 获取putLock
C1->>TakeLock: lock() 获取takeLock
par 并行操作
P1->>Queue: enqueue(node) 尾部插入
Queue-->>P1: count.getAndIncrement() 返回旧值 c
P1->>PutLock: 若 c+1 < capacity, notFull.signal()
P1->>PutLock: unlock()
and
C1->>Queue: dequeue() 头部移除
Queue-->>C1: count.getAndDecrement() 返回旧值 c
C1->>TakeLock: 若 c > 1, notEmpty.signal()
C1->>TakeLock: unlock()
end
Note over P1,C1: 入队出队几乎无竞争,并行执行
P1->>P1: 若 c==0, 获取 putLock 后 signalNotEmpty
C1->>C1: 若 c==capacity, 获取 takeLock 后 signalNotFull
图表说明:由于入队和出队持有不同的锁,只要队列非满非空,生产者和消费者可完全并行。级联唤醒(notFull.signal()、notEmpty.signal())在队列仍有空间或仍有元素时继续唤醒同类线程,提升响应速度。仅当状态在边界变化时(空变非空、满变非满),才会跨锁调用 signalNotEmpty/signalNotFull,此时需要获取另一把锁,产生短暂的锁竞争。
性能分析
- 时间复杂度:入队/出队 O(1),链表操作开销略高于数组索引,但无复制开销。
- 并发吞吐:双锁设计显著提升并发度,适合高并发生产者-消费者模型。在 JMH 基准测试中,多线程吞吐量通常比
ArrayBlockingQueue高 2~5 倍。 - 内存占用:每个元素需额外创建
Node对象,增加内存开销和 GC 压力。对于大量短期元素流,可能引发频繁 GC。
注意事项
- 默认无界容量风险:若未指定容量,默认
Integer.MAX_VALUE,生产速率持续大于消费速率时,队列将无限堆积导致内存溢出。最佳实践:始终显式指定合理容量。 - 双锁的代价:
size()和contains()等全遍历方法需要同时获取两把锁,开销较大,应避免频繁调用。 - 级联唤醒可能产生多余竞争:级联唤醒提高了响应性,但在特定场景下可能造成“惊群”效应,不过
LinkedBlockingQueue的实现已做了优化,每次只唤醒一个线程。
模块 7:SynchronousQueue 深度剖析——零容量的直接交付队列
SynchronousQueue 是一个极为特殊的阻塞队列,它内部不存储任何元素。生产者线程的 put 操作必须等待一个消费者线程的 take 操作来配对,反之亦然。它实现了生产者和消费者之间的直接交付(Direct Handoff),吞吐量极高,延迟极低。
Demo 代码(JDK 8 可运行)
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<String> queue = new SynchronousQueue<>();
// 生产者线程
Thread producer = new Thread(() -> {
try {
System.out.println("Producer: 准备交付 A");
queue.put("A");
System.out.println("Producer: A 已交付");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程 (延迟 2 秒后开始消费)
Thread consumer = new Thread(() -> {
try {
Thread.sleep(2000);
String item = queue.take();
System.out.println("Consumer: 接收到 " + item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
运行结果:生产者打印“准备交付 A”后阻塞,2 秒后消费者接收到 A,生产者打印“已交付”。
底层原理深入剖析
核心特性
SynchronousQueue 的容量恒为 0,isEmpty() 永远返回 true,size() 永远返回 0。其内部实现不维护任何存储元素的容器,而是维护等待配对的线程节点。
公平/非公平模式
- 公平模式:内部使用
TransferQueue(一个 FIFO 队列),严格按照先来先服务配对。 - 非公平模式(默认):内部使用
TransferStack(一个 LIFO 栈),后到的线程优先配对,有利于提高吞吐但可能导致饥饿。
非公平栈 TransferStack.transfer 源码级流程
Java 8 非公平模式下的核心方法 TransferStack.transfer,其节点模式有 REQUEST(消费者请求)、DATA(生产者提供数据)和 FULFILLING(正在匹配)。简化逻辑如下:
E transfer(E e, boolean timed, long nanos) {
SNode s = null;
int mode = (e == null) ? REQUEST : DATA;
for (;;) {
SNode h = head;
if (h == null || h.mode == mode) { // 栈空或模式相同
if (timed && nanos <= 0) { ... }
else if (casHead(h, s = new SNode(e, mode, h))) {
SNode m = awaitFulfill(s, timed, nanos); // 自旋或阻塞等待匹配
if (m == s) { clean(s); return null; }
...
return (E) m.item;
}
} else if (!isFulfilling(h.mode)) { // 模式互补且栈顶未在匹配中
if (h == null) ...
SNode mn = new SNode(e, mode, h);
if (casHead(h, mn)) {
match(mn, h); // 完成数据传递
return (E) h.item;
}
} else { // 栈顶正在匹配,帮助其完成
SNode nn = h.next;
if (casHead(h, nn)) ...
...
}
}
}
- 生产者(mode=DATA)到来时,若栈顶为空或为 DATA,则压栈并自旋/阻塞(调用
LockSupport.park)。 - 若栈顶为 REQUEST,则 CAS 改变栈顶为 FULFILLING 状态,然后匹配并交换数据,唤醒对方。
- 消费者逻辑对称。
下面的时序图描绘了 put 与 take 配对交付的过程,并结合 CAS 与 LockSupport:
sequenceDiagram
participant P as 生产者(put)
participant Stack as TransferStack
participant C as 消费者(take)
P->>Stack: transfer(e, DATA)
Stack->>Stack: 读取 head == null (栈空)
P->>Stack: CAS 压入 DATA 节点 (调用 casHead)
Stack-->>P: CAS 成功
P->>P: awaitFulfill : 自旋等待 + LockSupport.park
Note over P: 阻塞
C->>Stack: transfer(null, REQUEST)
Stack->>Stack: 读取 head.mode = DATA
C->>Stack: CAS 将 head 状态改为 FULFILLING 并压入 REQUEST 节点
Stack-->>C: CAS 成功
C->>Stack: 匹配: 将数据从 DATA 节点复制给消费者
C->>Stack: LockSupport.unpark(生产者)
C->>C: 返回数据
P->>P: 被唤醒,检查匹配结果
P->>Stack: 返回匹配的 item
图表说明:生产者先到达栈空,CAS 压入 DATA 节点后阻塞。消费者到达后发现栈顶模式互补,通过 CAS 将栈顶修改为匹配状态,完成数据传递并唤醒生产者。整个过程数据直接从生产者传输到消费者,不经过任何中间存储,CAS 与 LockSupport 保证了无锁下的线程安全。
性能分析
- 时间复杂度:配对操作为 O(1),但可能伴随自旋或阻塞。
- 空间消耗:几乎为零,仅维护线程节点。
- 并发吞吐:在匹配及时的场合下吞吐极高,因为完全避免了内存屏障和复制开销。
注意事项
- 不适用于需要缓冲的模型:若生产速率与消费速率不匹配,线程将频繁阻塞,上下文切换开销大。
- 典型应用:
CachedThreadPool:Executors.newCachedThreadPool()使用SynchronousQueue作为任务队列,每当新任务提交且无空闲线程时,会立即创建新线程处理,从而实现线程数按需伸缩。
模块 8:PriorityBlockingQueue 深度剖析——无界优先级阻塞队列
PriorityBlockingQueue 是无界的阻塞优先级队列,内部实现与 PriorityQueue 完全相同(基于数组二叉堆),但增加了并发控制。put 操作永不阻塞(因为队列无界),仅在 take 时若队列为空会阻塞。
Demo 代码(JDK 8 可运行)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
public class PriorityBlockingQueueDemo {
static class Task implements Comparable<Task> {
private final int priority;
private final String name;
Task(int priority, String name) { this.priority = priority; this.name = name; }
public int compareTo(Task o) { return Integer.compare(this.priority, o.priority); }
public String toString() { return name + "(" + priority + ")"; }
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Task> queue = new PriorityBlockingQueue<>();
// 生产者线程 (持续生产)
Thread producer = new Thread(() -> {
try {
queue.put(new Task(3, "Low"));
queue.put(new Task(1, "High"));
queue.put(new Task(2, "Medium"));
System.out.println("All tasks produced.");
} catch (Exception e) { e.printStackTrace(); }
});
// 消费者线程 (按优先级消费)
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
Task task = queue.take(); // 空时阻塞
System.out.println("Processing: " + task);
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
producer.start();
Thread.sleep(100); // 确保生产者先执行
consumer.start();
}
}
底层原理深入剖析
存储结构
与 PriorityQueue 共用相同的堆逻辑:Object[] queue 数组,默认容量 11,自动扩容。
并发控制
内部仅使用一把 ReentrantLock lock 保护所有访问,并配有一个条件 notEmpty。put 方法实际上调用 offer,由于无界,offer 仅需获取锁后进行堆插入,从不等待 notFull。take 在队列为空时调用 notEmpty.await() 阻塞,入队成功后调用 notEmpty.signal() 唤醒一个等待的消费者。
take 出队阻塞与堆操作源码级流程
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ((result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
private E dequeue() {
int n = size - 1;
if (n < 0) return null;
else {
Object[] array = queue;
E result = (E) array[0];
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
dequeue 返回 null 表示队列空,take 便进入 await。当生产者 offer 成功插入后,会在 finally 块中调用 notEmpty.signal() 唤醒消费者。
下面的流程图描绘了 take 出队时阻塞与堆下沉逻辑:
flowchart TD
A[调用 take] --> B[lock.lockInterruptibly 获取锁]
B --> C[dequeue 尝试出队]
C --> D{result == null?}
D -->|是| E[notEmpty.await 释放锁并阻塞]
E --> F[被生产者 signal 唤醒, 竞争锁]
F --> C
D -->|否| G[执行 siftDown 恢复堆]
G --> H[lock.unlock 释放锁]
H --> I[返回堆顶元素]
图表说明:take 方法在获取锁后通过 dequeue 尝试取元素,若返回 null 则在 notEmpty 条件上阻塞;一旦被生产者唤醒且重新获取锁,则完成出队、堆下沉并返回结果。由于 put 永远不会阻塞,阻塞仅发生在消费端。
性能分析
- 时间复杂度:入队
offer/putO(log n),出队takeO(log n),堆操作开销。 - 并发吞吐:单锁限制并发,但无界特性避免了生产者阻塞。扩容在锁内进行,可能短暂增加等待时间。
- 内存风险:无界队列可能导致内存溢出,需监控队列大小或使用自定义有界包装。
注意事项
- 无界风险:尽管
put不会阻塞,但若生产者速率持续高于消费者,队列将无限增长直至 OOM。务必做好流量控制或监控。 - 迭代器弱一致性:迭代器不保证实时视图,且不按优先级遍历。
Comparator一致性:与PriorityQueue相同,元素间比较必须与equals一致。
模块 9:DelayQueue 深度剖析——延迟队列与定时调度
DelayQueue 是一个无界的阻塞队列,其元素必须实现 Delayed 接口。只有当元素的延迟时间到期后,消费者才能从队列中取出它。它内部委托 PriorityQueue 按延迟时间排序,是定时任务调度、缓存过期清理等场景的理想选择。
Demo 代码(JDK 8 可运行)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayQueueDemo {
static class DelayedItem implements Delayed {
private final String name;
private final long expireTime; // 过期时间点 (纳秒)
DelayedItem(String name, long delay, TimeUnit unit) {
this.name = name;
this.expireTime = System.nanoTime() + unit.toNanos(delay);
}
@Override
public long getDelay(TimeUnit unit) {
long diff = expireTime - System.nanoTime();
return unit.convert(diff, TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
long diff = this.expireTime - ((DelayedItem) o).expireTime;
return diff < 0 ? -1 : (diff > 0 ? 1 : 0);
}
@Override
public String toString() { return name; }
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue<DelayedItem> queue = new DelayQueue<>();
// 模拟缓存过期清理
queue.put(new DelayedItem("Cache-A", 3, TimeUnit.SECONDS));
queue.put(new DelayedItem("Cache-B", 1, TimeUnit.SECONDS));
queue.put(new DelayedItem("Cache-C", 5, TimeUnit.SECONDS));
System.out.println("开始消费...");
while (!queue.isEmpty()) {
DelayedItem item = queue.take(); // 阻塞直到有元素到期
System.out.println("清理过期缓存: " + item + " at " + System.currentTimeMillis());
}
}
}
底层原理深入剖析
存储结构
DelayQueue 内部使用 PriorityQueue 作为容器,元素按延迟时间排序(堆顶为最早到期元素)。
阻塞与 Leader-Follower 模式源码级流程
take 方法源码核心逻辑(简化):
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
available.await();
} else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0) {
return q.poll();
}
first = null; // 不保活引用
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
Leader-Follower 模式避免多个线程同时限时等待:只有 leader 线程才执行 awaitNanos(delay),其他线程进入无限等待。当 leader 超时或被新插入的更早元素唤醒后,它重置 leader 并在 finally 中 signal 一个等待者成为新的 leader 或直接消费。
下面的状态图描绘了线程在 take 操作中的状态流转:
stateDiagram-v2
[*] --> 检查堆顶
检查堆顶 --> 为空: first == null
为空 --> 无限等待: available.await()
无限等待 --> 检查堆顶: 被唤醒 (offer 插入新元素)
检查堆顶 --> 计算延迟: first != null
计算延迟 --> 已到期: delay <= 0
已到期 --> 出队返回: q.poll()
出队返回 --> [*]
计算延迟 --> 未到期: delay > 0
未到期 --> 成为leader: leader == null
成为leader --> 限时等待: available.awaitNanos(delay)
限时等待 --> 重置leader: 超时或被新元素提前唤醒
重置leader --> 检查堆顶
未到期 --> 成为follower: leader != null
成为follower --> 无限等待: available.await()
无限等待 --> 检查堆顶: 被唤醒
图表说明:当多个线程调用 take 时,只有第一个到达且未到期的线程会成为 leader 并执行限时等待,其余线程作为 follower 无限期等待。当 leader 超时或被新元素入队提前唤醒后,会重置 leader 并唤醒一个 follower,新的 follower 重新检查堆顶。这种模式显著降低了 CPU 空转和无效唤醒。
性能分析
- 时间复杂度:入队 O(log n),出队在到期时 O(log n),等待期间受延迟时间影响。
- 并发控制:内部使用
ReentrantLock+Condition,单锁设计,但 Leader-Follower 模式减少了同时等待的线程数,优化了唤醒效率。 - 适用场景:定时任务调度、会话超时管理、缓存过期清理。
注意事项
getDelay应基于一致的时间源:建议使用System.nanoTime()而非System.currentTimeMillis(),避免系统时间调整影响。- 延迟时间不能为负:若
getDelay返回负数,元素会被立即消费,可能导致逻辑错误。 - 元素必须实现
Delayed且compareTo与getDelay一致:确保堆排序的正确性。
模块 10:ConcurrentLinkedQueue 深度剖析——无锁非阻塞并发队列
ConcurrentLinkedQueue 是基于 CAS 无锁算法实现的非阻塞线程安全队列,它是 Michael-Scott 并发队列算法的变体。它不实现 BlockingQueue 接口,因此没有阻塞方法,适合高并发、低延迟、非阻塞场景。
Demo 代码(JDK 8 可运行)
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConcurrentLinkedQueueDemo {
public static void main(String[] args) throws InterruptedException {
Queue<String> queue = new ConcurrentLinkedQueue<>();
// 多线程并发入队
Runnable producer = () -> {
for (int i = 0; i < 1000; i++) {
queue.offer(Thread.currentThread().getName() + "-" + i);
}
};
// 多线程并发出队
Runnable consumer = () -> {
String item;
while ((item = queue.poll()) != null) {
// 消费元素
}
};
Thread[] producers = new Thread[4];
Thread[] consumers = new Thread[4];
for (int i = 0; i < 4; i++) {
producers[i] = new Thread(producer);
consumers[i] = new Thread(consumer);
producers[i].start();
consumers[i].start();
}
for (int i = 0; i < 4; i++) {
producers[i].join();
consumers[i].join();
}
System.out.println("Final queue size: " + queue.size()); // 0
}
}
底层原理深入剖析
存储结构
基于单向链表,节点为 Node,包含 volatile E item 和 volatile Node next。使用 head 和 tail 两个指针,但并非总是指向真正的头尾节点,而是允许“滞后更新”以减少 CAS 竞争。
入队 offer:CAS 循环与 tail 滞后更新源码详解
JDK 8 的 offer 方法源码:
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) { // p 是最后一个节点
if (p.casNext(null, newNode)) { // CAS 链接新节点
if (p != t) // 每两次更新一次 tail
casTail(t, newNode); // 允许失败
return true;
}
} else if (p == q) // 遇到哨兵节点,重新定位
p = (t != (t = tail)) ? t : head;
else // 向后移动 p
p = (p != t && t != (t = tail)) ? t : q;
}
}
算法核心:通过 p 定位真正的尾节点,使用 CAS 将新节点链接到 p.next。tail 更新采用“懒”策略——仅当 tail 距离真正尾节点超过一个节点时尝试 CAS 更新,可接受失败(因为其他线程可能也更新了)。这种设计将热点分散到各个节点的 next 指针,减少对 tail 的竞争。
出队 poll:CAS 删除 head.item
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) { // CAS 逻辑删除
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
} else if ((q = p.next) == null) {
updateHead(h, p);
return null;
} else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
poll 从头部开始,CAS 将节点 item 置为 null 表示逻辑删除,然后更新 head(滞后)。遇到哨兵节点(p == q)会重新定位。
下面的时序图描绘了多线程 CAS 入队与 tail 滞后更新的过程:
sequenceDiagram
participant T1 as 线程1 (入队 A)
participant T2 as 线程2 (入队 B)
participant Q as ConcurrentLinkedQueue
Note over Q: tail 指向 dummy 节点
T1->>Q: 读取 tail, 找尾节点 p (p.next==null)
T1->>Q: CAS(p.next, null, Node A)
Q-->>T1: CAS 成功
T1->>Q: 尝试 CAS 更新 tail (p != t 成立)
Note over Q: tail 可能被 T1 更新,也可能滞后
T2->>Q: 读取 tail (可能仍指向旧尾)
T2->>Q: 遍历 next 链找到真正尾节点
T2->>Q: CAS(尾.next, null, Node B)
Q-->>T2: CAS 成功
T2->>Q: 尝试 CAS 更新 tail (p != t 成立)
Note over Q: 最终 tail 指向 Node B 或稍前节点
图表说明:线程 T1 与 T2 并发入队,通过 CAS 在不同尾节点的 next 上操作,避免了在 tail 指针上的直接竞争。tail 允许短暂滞后,后续线程通过遍历 next 链定位真正的尾节点,从而保证了并发正确性和高吞吐。
性能分析
- 时间复杂度:均摊 O(1),CAS 失败时会有自旋重试,但无阻塞。
- 并发吞吐:无锁设计使其在极高并发下吞吐量优秀,延迟极低,适合低延迟要求的网络、消息中间件场景。多核 CPU 下 CAS 自旋开销可控。
- 内存占用:链表结构,每个元素有
Node对象开销,且由于逻辑删除的节点可能未被即时回收,需要 GC。
注意事项
- 无界需控速:若生产者速率持续大于消费者,队列会无限增长导致 OOM。需在业务层面限流或使用有界队列。
size()O(n) 弱一致性:size()方法需遍历整个链表计算元素数量,时间复杂度 O(n),且在高并发下返回值为近似值。应避免频繁调用。- 不支持阻塞:需要阻塞语义的场景应使用
BlockingQueue实现。 - 迭代器弱一致性:迭代器不保证反映实时修改,且不会抛出
ConcurrentModificationException。
模块 11:面试高频专题深度解析
本模块集中所有面试相关内容,正文其他部分不涉及任何面试考点。
1. BlockingQueue 的 put/take 与 offer/poll 的区别及阻塞实现原理
题目背景
生产者-消费者模型是并发编程中的经典模式,Java 通过 BlockingQueue 提供了开箱即用的实现。理解阻塞与非阻塞方法的区别及底层实现是评估候选人并发功底的试金石。
标准回答
put(E e)和take()是阻塞方法。当队列满时,put会使当前线程等待,直到队列有空间;当队列空时,take会使当前线程等待,直到有元素可用。offer(E e)和poll()是非阻塞方法(特殊值版)。offer在队列满时立即返回false;poll在队列空时立即返回null。- 带超时的
offer(e, time, unit)和poll(time, unit)在指定时间内阻塞,超时返回false/null。
阻塞实现原理:以 ArrayBlockingQueue 为例,内部使用 ReentrantLock 和两个 Condition(notEmpty、notFull)。put 方法获取锁后检查队列是否满,若满则调用 notFull.await() 释放锁并阻塞;当消费者取出元素后,会调用 notFull.signal() 唤醒一个等待的生产者。被唤醒的线程重新竞争锁,成功获取后继续执行插入。
追问模拟
-
追问:
Condition的await()和signal()与Object的wait()和notify()有何区别?为什么使用Condition?- 回答:
Condition提供了更灵活的等待/通知机制,一个Lock可以创建多个Condition,从而实现精确唤醒特定条件的线程(如notFull只唤醒生产者,notEmpty只唤醒消费者)。wait/notify只能绑定单一等待队列,且notify可能唤醒不相关的线程,造成无效竞争。
- 回答:
-
追问:
LinkedBlockingQueue的双锁设计中,如何防止生产者唤醒生产者、消费者唤醒消费者导致的无效唤醒?- 回答:
LinkedBlockingQueue通过putLock和takeLock分离入队出队锁,且各自有独立的Condition。入队操作只在队列由空变非空时唤醒消费者,或由满变非满时唤醒生产者;出队对称。代码中通过c == 0或c == capacity条件判断,精确控制唤醒对象,避免了跨角色唤醒。
- 回答:
-
追问:如果一个线程在
put时被中断,会发生什么?- 回答:
put方法响应中断,会抛出InterruptedException。在await期间被中断时,Condition会将线程从条件队列转移到同步队列,待线程获取锁后抛出异常,同时线程的中断状态会被清除。
- 回答:
加分回答建议
- 提及
LockSupport.park/unpark是Condition底层依赖的线程阻塞原语,AQS框架中条件队列的实现细节。 - 讨论
SynchronousQueue采用的无锁配对算法,与基于锁的条件队列的本质区别。
2. ArrayBlockingQueue 与 LinkedBlockingQueue 的锁设计对比及性能差异
题目背景
两者是最常用的有界阻塞队列,在面试中常被要求对比分析选型依据。
标准回答
- 锁设计:
ArrayBlockingQueue使用单锁(一把ReentrantLock)保护所有操作,配有两个Condition。LinkedBlockingQueue使用双锁(putLock和takeLock)分离入队和出队操作。 - 并发度:
LinkedBlockingQueue的入队和出队可以并行,因此在高并发场景下吞吐量通常高于ArrayBlockingQueue。ArrayBlockingQueue的生产者和消费者在同一时刻只能有一个在操作。 - 内存与 GC:
ArrayBlockingQueue内部使用固定数组,内存占用连续,无额外对象分配,GC 友好。LinkedBlockingQueue每个元素需要创建Node对象,频繁插入删除会产生大量临时对象,加重 GC 负担。
追问模拟
-
追问:
LinkedBlockingQueue的双锁设计是否完全消除了锁竞争?- 回答:并未完全消除。当队列满或空时,生产者和消费者仍需通过条件队列通信,且跨锁唤醒(
signalNotEmpty/signalNotFull)时需要获取另一把锁,此时会有锁的传递与线程上下文切换。此外,size()、contains()等遍历方法需要同时获取两把锁,会阻塞所有操作。
- 回答:并未完全消除。当队列满或空时,生产者和消费者仍需通过条件队列通信,且跨锁唤醒(
-
追问:什么场景下
ArrayBlockingQueue可能比LinkedBlockingQueue表现更好?- 回答:在生产者与消费者数量较少、操作不频繁,或者内存敏感、追求低 GC 压力的嵌入式系统中,
ArrayBlockingQueue因其内存连续、无额外对象开销,性能更稳定,且可预测性强。
- 回答:在生产者与消费者数量较少、操作不频繁,或者内存敏感、追求低 GC 压力的嵌入式系统中,
-
追问:
LinkedBlockingQueue的默认容量是Integer.MAX_VALUE,使用中需要注意什么?- 回答:务必显式指定合理容量,避免生产者速率持续大于消费者时,队列无限膨胀导致 OOM。实际工程中,容量应结合系统处理能力和内存资源评估。
加分回答建议
- 分析两者在 JMH 基准测试下的实际吞吐量差异,给出数量级参考。
- 讨论双锁设计中的“级联唤醒”优化及其对性能的微妙影响。
3. SynchronousQueue 的工作原理及在 CachedThreadPool 中的应用
题目背景
SynchronousQueue 是线程池 CachedThreadPool 的核心组件,理解其“零容量”特性是理解线程池伸缩机制的关键。
标准回答
SynchronousQueue 内部不存储任何元素,其 put 操作必须等待一个 take 操作来配对交付,反之亦然。内部通过无锁的栈(非公平)或队列(公平)管理等待配对的线程。配对成功时,数据直接从生产者线程移交给消费者线程,无内存拷贝。
在 CachedThreadPool 中,任务队列即为 SynchronousQueue。当提交新任务时:
- 若有空闲线程正在等待任务(即执行了
take),则任务立即交付给该线程执行。 - 若无空闲线程,则
offer失败,线程池会创建新线程处理任务(不超过最大线程数限制)。 这种机制使得线程池能够根据任务提交速率动态伸缩线程数量,适合处理大量短期异步任务。
追问模拟
-
追问:
SynchronousQueue公平模式和非公平模式有何区别?分别适用什么场景?- 回答:公平模式内部使用 FIFO 队列,保证先到达的线程先配对,避免饥饿,但吞吐稍低。非公平模式(默认)使用 LIFO 栈,后到达的线程先配对,有利于利用 CPU 缓存热度,吞吐更高,但可能导致部分线程长期等待。对于
CachedThreadPool,非公平模式吞吐更好,因为任务处理时间通常很短,饥饿概率低。
- 回答:公平模式内部使用 FIFO 队列,保证先到达的线程先配对,避免饥饿,但吞吐稍低。非公平模式(默认)使用 LIFO 栈,后到达的线程先配对,有利于利用 CPU 缓存热度,吞吐更高,但可能导致部分线程长期等待。对于
-
追问:
SynchronousQueue如何实现数据直接从生产者移交给消费者?- 回答:内部节点(如
TransferStack.SNode)持有数据引用。生产者线程将数据放入节点并压栈,然后自旋/阻塞;消费者线程发现匹配节点后,通过 CAS 将节点状态改为匹配,并将节点数据读出,同时唤醒生产者。数据从未进入一个“队列容器”,而是在线程栈间直接传递。
- 回答:内部节点(如
-
追问:如果消费者线程极少,大量生产者线程会在
SynchronousQueue上阻塞,会有什么风险?- 回答:会迅速耗尽系统线程资源,导致无法创建新线程(对于
CachedThreadPool还会不断创建线程直至 OOM)。因此SynchronousQueue必须用在生产与消费速率大致匹配,或能够通过其他方式(如线程池拒绝策略)限制并发度的场景。
- 回答:会迅速耗尽系统线程资源,导致无法创建新线程(对于
加分回答建议
- 深入讲解
TransferStack的三种模式(REQUEST、DATA、FULFILLING)及状态转换。 - 对比
LinkedTransferQueue的transfer方法,说明SynchronousQueue的特殊性。
4. PriorityQueue 的堆实现与 PriorityBlockingQueue 的并发扩展
题目背景
优先级队列是算法与数据结构基础,面试中常考察二叉堆原理及并发环境下的适配。
标准回答
PriorityQueue 基于数组实现小顶堆(默认),通过 siftUp(上浮)和 siftDown(下沉)维持堆性质。入队时元素追加到数组末尾并上浮;出队时移除堆顶,将末尾元素移至堆顶并下沉。扩容策略为:容量<64 时翻倍,≥64 时增加 50%。
PriorityBlockingQueue 在 PriorityQueue 基础上增加了并发控制,使用 ReentrantLock 保护所有操作,put 永不阻塞(无界),take 在队列为空时通过 Condition 阻塞。扩容过程在锁保护下进行。
追问模拟
-
追问:
PriorityQueue的迭代器为什么不按优先级顺序遍历?- 回答:
PriorityQueue的迭代器直接按内部数组顺序遍历(即堆的层序遍历),而非优先级顺序。这是因为按优先级遍历需要反复执行poll操作(会破坏堆结构),若要在不破坏堆的前提下按序输出,需额外排序,开销较大。迭代器设计为快速失败,仅用于只读遍历元素本身。
- 回答:
-
追问:为什么
PriorityBlockingQueue不提供notFull条件?- 回答:因为它是无界队列,
put操作永远不会因队列满而阻塞(除非内存耗尽),所以无需notFull条件。这也是无界阻塞队列的典型特征。
- 回答:因为它是无界队列,
-
追问:如何实现大顶堆的
PriorityQueue?- 回答:通过构造函数传入自定义
Comparator,例如Comparator.reverseOrder()或自定义比较逻辑,使得堆顶成为最大元素。
- 回答:通过构造函数传入自定义
加分回答建议
- 讨论堆排序算法及建堆的 O(n) 时间复杂度证明。
- 分析
PriorityBlockingQueue在扩容时的锁竞争问题,以及如何通过批量操作减少锁竞争。
5. DelayQueue 的延迟阻塞机制与 Leader-Follower 模式
题目背景
DelayQueue 是定时任务调度的基础组件,其内部的 Leader-Follower 模式是减少无效等待线程数量的经典设计。
标准回答
DelayQueue 内部使用 PriorityQueue 按元素延迟时间排序。take 方法会检查堆顶元素的剩余延迟:
- 若延迟 ≤ 0,立即出队返回。
- 若延迟 > 0,则进入等待。为了避免多个消费者线程同时进行限时等待造成大量
park/unpark开销,DelayQueue采用 Leader-Follower 模式:只有一个线程被选为leader,它负责限时等待(available.awaitNanos(delay)),其余线程作为follower无限期等待(available.await())。当leader超时或被新入队元素提前唤醒后,会重置leader并唤醒一个follower,该follower重新竞争成为新leader或直接消费。
追问模拟
-
追问:为什么新元素入队时要唤醒
leader?- 回答:因为新入队元素的延迟可能比当前堆顶元素更短(成为新堆顶),若
leader正在等待较长时间,会被提前唤醒重新计算延迟,从而保证延迟准确性。
- 回答:因为新入队元素的延迟可能比当前堆顶元素更短(成为新堆顶),若
-
追问:如果
leader线程在awaitNanos期间被中断,会发生什么?- 回答:
awaitNanos响应中断,会抛出InterruptedException。在抛出前,leader线程会将leader置为null并唤醒一个follower,以保持队列的可用性。
- 回答:
-
追问:
DelayQueue的drainTo方法会排出未到期的元素吗?- 回答:不会。
drainTo仅排出当前已到期的元素,内部通过循环peek检查堆顶延迟是否 ≤0,若是则出队,否则停止。未到期元素不会被排出。
- 回答:不会。
加分回答建议
- 提及
ScheduledThreadPoolExecutor的内部DelayedWorkQueue采用了类似的 Leader-Follower 模式。 - 讨论为什么使用
System.nanoTime()而非currentTimeMillis()。
6. ConcurrentLinkedQueue 的无锁 CAS 实现及 tail 滞后更新设计意图
题目背景
无锁并发队列是高性能中间件(如 Netty、Disruptor)的基石,考察 CAS 算法理解能力。
标准回答
ConcurrentLinkedQueue 使用 CAS 实现 Michael-Scott 非阻塞队列算法。入队时,通过 CAS 将新节点链接到尾节点的 next 指针上。tail 指针并非实时更新,而是允许滞后:当 tail 距离真正尾节点超过一个节点时,才通过 CAS 尝试更新。出队时,head 指针同样滞后更新。
滞后更新设计意图:减少对 tail/head 指针的 CAS 竞争。因为多个线程并发入队时,若每个线程都试图更新 tail,会导致大量 CAS 失败和重试。滞后更新将竞争分散到各个节点的 next 指针上,使得多个入队线程可以同时在不同的尾节点后链接新节点,提升了并发度。
追问模拟
-
追问:如果
tail滞后,后续入队线程如何找到真正的尾节点?- 回答:通过遍历
tail.next链。算法中有一个“定位尾节点”的循环:从当前tail开始,若其next不为null,则向后移动,直至找到next == null的节点作为真正尾节点。
- 回答:通过遍历
-
追问:
size()方法为什么是 O(n)?在高并发下返回的值准确吗?- 回答:
size()需遍历整个链表计算节点数(排除逻辑删除的节点),时间复杂度 O(n)。由于遍历期间可能有其他线程插入或删除,返回值仅是一个近似值,具有弱一致性。官方文档也明确说明此点。
- 回答:
-
追问:
ConcurrentLinkedQueue与LinkedBlockingQueue在实现上有哪些根本区别?- 回答:前者使用 CAS 非阻塞算法,无锁、无阻塞;后者使用
ReentrantLock+Condition,支持阻塞操作。前者无界,后者可指定有界。前者size()弱一致,后者size()通过锁保证强一致(但双锁下需要加两把锁)。
- 回答:前者使用 CAS 非阻塞算法,无锁、无阻塞;后者使用
加分回答建议
- 介绍
ConcurrentLinkedQueue的HOPS延迟更新常量(HOPS = 1),解释其调优意义。 - 讨论“逻辑删除”节点(
item置为null)与 GC 的关系。
7. 线程池任务队列选型:ArrayBlockingQueue vs LinkedBlockingQueue vs SynchronousQueue
题目背景
线程池是日常开发最常用的并发工具,其任务队列的选择直接影响线程池行为。面试常考根据场景选型的能力。
标准回答
| 队列类型 | 有界性 | 锁机制 | 适用场景 |
|---|---|---|---|
ArrayBlockingQueue | 强制有界 | 单锁 | 需要严格控制内存占用,任务量稳定可预测的场景。 |
LinkedBlockingQueue | 可选有界 | 双锁 | 高并发任务处理,但必须显式指定容量防止 OOM,如 Web 服务器请求排队。 |
SynchronousQueue | 零容量 | 无锁配对 | 任务处理时间短、需要快速响应,且线程数可按需伸缩的场景(如 CachedThreadPool)。 |
PriorityBlockingQueue | 无界 | 单锁 | 任务有优先级要求的场景。 |
DelayQueue | 无界 | 单锁+Leader-Follower | 定时任务、缓存过期处理。 |
追问模拟
-
追问:在
FixedThreadPool中,为什么使用LinkedBlockingQueue而不使用ArrayBlockingQueue?- 回答:
FixedThreadPool的核心线程数固定,最大线程数等于核心线程数,它依赖于无界队列来缓存超出核心线程数的任务。Executors.newFixedThreadPool默认使用LinkedBlockingQueue的无界特性,若改用有界队列,则需配合拒绝策略,否则任务会被拒绝。
- 回答:
-
追问:如果希望线程池在队列满时,让提交任务的线程自己执行该任务,应如何配置?
- 回答:使用有界队列(如
ArrayBlockingQueue),并设置拒绝策略为ThreadPoolExecutor.CallerRunsPolicy。当队列满时,execute方法会由调用者线程直接执行任务,从而减缓任务提交速率。
- 回答:使用有界队列(如
-
追问:在什么情况下你会选择
SynchronousQueue配合ThreadPoolExecutor自定义线程池?- 回答:当任务具有突发性且处理迅速,同时希望线程池能够根据负载动态伸缩线程数,但又想限制最大线程数时。例如设置
corePoolSize=0,maximumPoolSize=N,keepAliveTime=60s,SynchronousQueue。任务到达时若无空闲线程则创建新线程,空闲线程 60 秒后回收,避免无限创建。
- 回答:当任务具有突发性且处理迅速,同时希望线程池能够根据负载动态伸缩线程数,但又想限制最大线程数时。例如设置
加分回答建议
- 结合《Java 并发编程实战》中关于线程池任务队列选型的经典案例分析。
- 提及
LinkedTransferQueue作为SynchronousQueue与LinkedBlockingQueue的折中方案。
8. ArrayDeque 作为栈/队列优于 LinkedList/Stack 的原因
题目背景
Stack 类已过时,ArrayDeque 被官方推荐为替代品。面试考察对 API 演进的理解和性能敏感度。
标准回答
- 性能:
ArrayDeque基于循环数组,头尾操作 O(1),无节点分配开销;LinkedList每次插入需创建Node对象,增加内存和 GC 压力。Stack继承自Vector,所有方法均使用synchronized同步,即使单线程环境也有不必要的锁开销。 - 内存效率:
ArrayDeque数组连续存储,CPU 缓存命中率高;LinkedList节点分散在堆中,遍历时易导致缓存未命中。 - API 设计:
Deque接口统一了栈和队列操作(push/pop与offer/poll),语义清晰。Stack继承Vector暴露了不应在栈上使用的insertElementAt等方法,破坏封装。
追问模拟
-
追问:
ArrayDeque为什么不允许null元素?- 回答:
ArrayDeque使用null作为特殊标记表示队列为空(poll和peek返回null),若允许插入null则无法区分空队列和有效null元素。此外,null会破坏removeFirstOccurrence等依赖equals的方法。
- 回答:
-
追问:
ArrayDeque的扩容机制具体是如何实现的?- 回答:当队列满时,创建一个容量为原数组两倍的新数组,然后通过两次
System.arraycopy将原循环数组中的元素按顺序复制到新数组的起始位置(先拷贝head到数组末尾的部分,再拷贝0到tail的部分),最后重置head=0,tail=size。
- 回答:当队列满时,创建一个容量为原数组两倍的新数组,然后通过两次
-
追问:
ArrayDeque为什么比LinkedList更适合作为队列使用,即使理论上两者头尾操作都是 O(1)?- 回答:常数因子差异巨大。数组操作仅涉及索引计算和直接赋值,而链表操作涉及内存分配、指针修改和可能的缓存失效。在微基准测试中,
ArrayDeque的吞吐量通常是LinkedList的数倍。
- 回答:常数因子差异巨大。数组操作仅涉及索引计算和直接赋值,而链表操作涉及内存分配、指针修改和可能的缓存失效。在微基准测试中,
加分回答建议
- 对比
ArrayDeque与LinkedList在迭代遍历时的性能差异(数组遍历 vs 指针跳转)。 - 提及
ArrayDeque的spliterator并行遍历能力。
9. TransferQueue(LinkedTransferQueue)的 transfer 方法与 put 的区别
题目背景
TransferQueue 是 Java 7 引入的增强接口,其 transfer 方法提供了更精细的交付控制,在反应式编程和高性能消息传递中应用广泛。
标准回答
put(E e):来自BlockingQueue,仅将元素放入队列,若队列满则阻塞等待空间,放入后立即返回,不关心元素何时被消费。transfer(E e):来自TransferQueue,不仅将元素放入队列,还要求必须有一个消费者接收该元素后,方法才会返回。若没有等待的消费者,transfer会阻塞直到元素被取走。这相当于“同步交付”,但不同于SynchronousQueue的零容量,LinkedTransferQueue内部可以有未匹配的元素。
追问模拟
-
追问:
tryTransfer和transfer有何区别?- 回答:
tryTransfer是非阻塞版本。若存在等待的消费者,则立即交付并返回true;否则立即返回false,元素不会被放入队列(除非另有重载方法指定了超时或允许入队)。
- 回答:
-
追问:
LinkedTransferQueue与SynchronousQueue的区别是什么?- 回答:
SynchronousQueue容量为 0,无法存储元素,每个生产者必须对应一个消费者。LinkedTransferQueue可以存储元素(无界),transfer方法既可以像SynchronousQueue一样等待消费者,也可以通过put/offer将元素暂存队列中供后续消费。LinkedTransferQueue是二者的超集。
- 回答:
-
追问:什么场景下适合使用
LinkedTransferQueue?- 回答:当希望“尽量直接交付,但允许在无消费者时暂存”的场景。例如日志处理系统,日志生产者希望尽快将日志交给处理器,但如果处理器暂时忙碌,日志可以先入队缓冲,避免生产者阻塞。
加分回答建议
- 介绍
LinkedTransferQueue基于xs和matches的无锁实现原理(Dual Queue 算法)。 - 提及 Netty 的
NioEventLoop中任务队列采用了类似于LinkedTransferQueue的 Mpsc 队列。
10. 各种队列的时间复杂度一览与选型决策树
题目背景
系统化总结各队列操作的复杂度,帮助快速定位选型,是考察综合能力的常见题目。
标准回答
| 队列类型 | 入队 | 出队 | 检查 | 阻塞特性 | 有界性 | 线程安全 |
|---|---|---|---|---|---|---|
ArrayDeque | O(1) | O(1) | O(1) | 否 | 自动扩容 | 否 |
PriorityQueue | O(log n) | O(log n) | O(1) | 否 | 自动扩容 | 否 |
ArrayBlockingQueue | O(1) | O(1) | O(1) | 是(满/空阻塞) | 有界 | 是 |
LinkedBlockingQueue | O(1) | O(1) | O(1) | 是(满/空阻塞) | 可选有界 | 是 |
SynchronousQueue | 配对 O(1) | 配对 O(1) | 无 | 是(等待配对) | 零容量 | 是 |
PriorityBlockingQueue | O(log n) | O(log n) | O(1) | 是(空阻塞) | 无界 | 是 |
DelayQueue | O(log n) | O(log n) | O(1) | 是(到期阻塞) | 无界 | 是 |
ConcurrentLinkedQueue | 均摊 O(1) | 均摊 O(1) | O(1) | 否 | 无界 | 是 |
LinkedTransferQueue | 均摊 O(1) | 均摊 O(1) | O(1) | 可选 | 无界 | 是 |
选型决策树(文字描述):
- 是否需要阻塞等待?
- 否 → 是否需要线程安全?
- 否 → 是否需要双端操作或高性能栈/队列? →
ArrayDeque - 是 → 是否允许无界? → 是 →
ConcurrentLinkedQueue;否 → 自定义有界包装。
- 否 → 是否需要双端操作或高性能栈/队列? →
- 是 → 是否有优先级要求?
- 是 → 是否需要延迟调度? → 是 →
DelayQueue;否 →PriorityBlockingQueue - 否 → 是否允许暂存元素?
- 否(必须直接交付)→
SynchronousQueue - 是 → 容量是否需要固定? → 固定 →
ArrayBlockingQueue;高并发 →LinkedBlockingQueue(指定容量);需要灵活交付 →LinkedTransferQueue
- 否(必须直接交付)→
- 是 → 是否需要延迟调度? → 是 →
- 否 → 是否需要线程安全?
下面的 Mermaid 流程图直观展示了队列选型决策树:
flowchart TD
Start[开始选型] --> Q1{需要阻塞等待?}
Q1 -->|否| Q2{需要线程安全?}
Q2 -->|否| R1[ArrayDeque]
Q2 -->|是| Q3{是否允许无界?}
Q3 -->|是| R2[ConcurrentLinkedQueue]
Q3 -->|否| R3[自定义有界包装或ConcurrentLinkedQueue+限流]
Q1 -->|是| Q4{是否有优先级/延迟要求?}
Q4 -->|优先级| R4[PriorityBlockingQueue]
Q4 -->|延迟| R5[DelayQueue]
Q4 -->|无| Q5{是否允许暂存元素?}
Q5 -->|否,必须直接交付| R6[SynchronousQueue]
Q5 -->|是| Q6{需要固定容量与内存可预测?}
Q6 -->|是| R7[ArrayBlockingQueue]
Q6 -->|否,追求高吞吐| Q7{是否需要灵活的交付语义?}
Q7 -->|是| R8[LinkedTransferQueue]
Q7 -->|否| R9[LinkedBlockingQueue]
图表说明:决策树从核心需求出发,依次判断阻塞、优先级、容量等关键特性,最终引导至合适的队列实现类。例如,高性能非阻塞场景首选 ArrayDeque 或 ConcurrentLinkedQueue;阻塞场景中,若需优先级则选 PriorityBlockingQueue,若需延迟则选 DelayQueue,若追求吞吐且允许暂存则选 LinkedBlockingQueue。
模块 12:实战陷阱与最佳实践(附完整 Demo)
陷阱 1:无界 LinkedBlockingQueue 导致内存溢出 → 指定容量
错误示例:
import java.util.concurrent.*;
public class UnboundedQueueOOM {
public static void main(String[] args) {
// 危险:默认无界队列
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 2, 0L, TimeUnit.MILLISECONDS, queue);
// 持续提交任务,消费速率远低于生产速率
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.submit(() -> {
try { Thread.sleep(10000); } catch (InterruptedException e) {}
return null;
});
}
// 最终 OOM
}
}
正确写法:
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(1000); // 显式指定容量
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 2, 0L, TimeUnit.MILLISECONDS, queue,
new ThreadPoolExecutor.CallerRunsPolicy()); // 配合拒绝策略
陷阱 2:ThreadPoolExecutor 误用 SynchronousQueue 导致任务拒绝
错误示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
// 提交第一个任务,被唯一线程处理,后续任务无空闲线程,立即触发拒绝策略
executor.submit(() -> { Thread.sleep(10000); return null; });
executor.submit(() -> { return null; }); // RejectedExecutionException
正确写法:配合适当的 maximumPoolSize 和拒绝策略,或使用 LinkedBlockingQueue 缓冲。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
0, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue<>());
陷阱 3:PriorityQueue 多线程并发修改 → 改用 PriorityBlockingQueue
错误示例:
PriorityQueue<Integer> pq = new PriorityQueue<>();
// 多线程并发 offer/poll 导致堆结构破坏,元素丢失或死循环
正确写法:
BlockingQueue<Integer> pq = new PriorityBlockingQueue<>();
陷阱 4:DelayQueue 中 getDelay 返回负数导致元素立即消费
错误示例:
class BadDelayed implements Delayed {
public long getDelay(TimeUnit unit) {
return -1; // 永远返回负数,元素立即被视为到期,破坏延迟语义
}
}
正确写法:基于一致的时间源计算剩余延迟。
class GoodDelayed implements Delayed {
private final long expireTime = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.nanoTime(), TimeUnit.NANOSECONDS);
}
}
陷阱 5:ConcurrentLinkedQueue 的 size() 在并发场景下的弱一致性陷阱
错误示例:
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
// 多线程并发入队出队
if (queue.size() > 0) {
String s = queue.poll(); // 可能为 null,因为 size() 是近似值
}
正确写法:直接使用 poll() 检查返回值是否为 null,避免依赖 size()。
String s;
while ((s = queue.poll()) != null) {
// 处理
}
陷阱 6:ArrayBlockingQueue 单锁设计在高并发下的性能瓶颈识别
问题代码:高并发场景下,大量线程争用单锁导致上下文切换激增。
优化建议:
- 若业务允许,切换为
LinkedBlockingQueue(双锁)提升并发度。 - 若必须使用
ArrayBlockingQueue,可考虑采用多个队列分片(如List<BlockingQueue>),根据线程 ID 哈希分散写入。
模块 13:时间复杂度总结与队列选型决策树
操作复杂度对照表
| 队列类型 | 入队 | 出队 | 检查 | 阻塞特性 | 有界性 | 线程安全 |
|---|---|---|---|---|---|---|
ArrayDeque | O(1) | O(1) | O(1) | 否 | 自动扩容 | 否 |
PriorityQueue | O(log n) | O(log n) | O(1) | 否 | 自动扩容 | 否 |
ArrayBlockingQueue | O(1) | O(1) | O(1) | 是(满/空阻塞) | 有界 | 是 |
LinkedBlockingQueue | O(1) | O(1) | O(1) | 是(满/空阻塞) | 可选有界 | 是 |
SynchronousQueue | 配对 O(1) | 配对 O(1) | 无 | 是(等待配对) | 零容量 | 是 |
PriorityBlockingQueue | O(log n) | O(log n) | O(1) | 是(空阻塞) | 无界 | 是 |
DelayQueue | O(log n) | O(log n) | O(1) | 是(到期阻塞) | 无界 | 是 |
ConcurrentLinkedQueue | 均摊 O(1) | 均摊 O(1) | O(1) | 否 | 无界 | 是 |
LinkedTransferQueue | 均摊 O(1) | 均摊 O(1) | O(1) | 可选 | 无界 | 是 |
队列选型决策树流程图
flowchart TD
Start[开始选型] --> Q1{需要阻塞等待?}
Q1 -->|否| Q2{需要线程安全?}
Q2 -->|否| R1[ArrayDeque]
Q2 -->|是| Q3{是否允许无界?}
Q3 -->|是| R2[ConcurrentLinkedQueue]
Q3 -->|否| R3[自定义有界包装或ConcurrentLinkedQueue+限流]
Q1 -->|是| Q4{是否有优先级/延迟要求?}
Q4 -->|优先级| R4[PriorityBlockingQueue]
Q4 -->|延迟| R5[DelayQueue]
Q4 -->|无| Q5{是否允许暂存元素?}
Q5 -->|否,必须直接交付| R6[SynchronousQueue]
Q5 -->|是| Q6{需要固定容量与内存可预测?}
Q6 -->|是| R7[ArrayBlockingQueue]
Q6 -->|否,追求高吞吐| Q7{是否需要灵活的交付语义?}
Q7 -->|是| R8[LinkedTransferQueue]
Q7 -->|否| R9[LinkedBlockingQueue]
图表说明:该决策树将队列选型过程结构化,帮助开发者在面对具体需求时快速定位合适的队列实现。从是否需要阻塞开始,逐步细化到优先级、容量、吞吐等维度,最终指向最匹配的类。
模块 14:Queue 的并发控制深度对比与实战分析
前述各模块已对每种队列的并发原语进行了剖析,这里我们将横向对比它们在并发控制策略上的选择逻辑、性能权衡以及在不同负载下的行为,帮助你在架构层面做出精准判断。
14.1 并发控制策略分类
| 队列 | 并发控制策略 | 锁/原语 | 特点 |
|---|---|---|---|
ArrayBlockingQueue | 单锁双条件 | ReentrantLock + 2 Condition | 结构简单,竞争强 |
LinkedBlockingQueue | 双锁双条件 | putLock + takeLock,各自 Condition | 入队出队分离,吞吐高 |
PriorityBlockingQueue | 单锁单条件 | ReentrantLock + notEmpty | 无界,put 永不阻塞 |
DelayQueue | 单锁 + Leader-Follower | ReentrantLock + Condition | 减少限时等待线程数 |
SynchronousQueue | 无锁配对(栈/队列) | CAS + LockSupport | 零容量,直接交付 |
ConcurrentLinkedQueue | 无锁 CAS + 自旋 | CAS (Unsafe) | 非阻塞,均摊 O(1) |
LinkedTransferQueue | 无锁 mixed mode | CAS + LockSupport | 结合同步交付与缓冲 |
选型启示:
- 若需阻塞等待且容量有界,
ArrayBlockingQueue适合低并发或内存紧张场景;LinkedBlockingQueue在 4 核以上、8+ 线程时吞吐显著领先。 - 若追求极致低延迟,且允许 CPU 自旋开销,
ConcurrentLinkedQueue或SynchronousQueue更优。 - 若逻辑需要优先级或延迟,只能选择对应的阻塞版本,并接受单锁吞吐上限。
14.2 锁竞争与缓存行影响
- 单锁队列(
ArrayBlockingQueue、PriorityBlockingQueue):所有操作竞争同一把锁,当 CPU 核数增加时,上下文切换和缓存行失效成为瓶颈。可通过分片(List<ArrayBlockingQueue>)降低竞争。 - 双锁队列(
LinkedBlockingQueue):putLock和takeLock分离,但count是AtomicInteger,会被两个锁频繁更新,仍存在一定缓存行抖动。实际吞吐量通常能支撑 10w+ TPS。 - 无锁队列(
ConcurrentLinkedQueue):CAS 操作直接修改共享变量,无上下文切换,但高竞争下 CAS 自旋会消耗 CPU,且频繁修改head/tail仍可能导致缓存行失效。JDK 后续版本引入VarHandle优化了部分问题。
14.3 伪共享与填充优化
在 LinkedBlockingQueue 等实现中,putLock 和 takeLock 是独立对象,理论上可能存在伪共享(它们常被同一生产者/消费者线程交替访问,位于同一缓存行时会导致互相失效)。但通常 JVM 对象内存布局会使得不同对象不太可能处于同一缓存行,因此影响较小。更值得关注的是 AtomicInteger count 的位置,频繁的两端更新仍可能在同一缓存行“乒乓”。
建议:除非明确观测到性能瓶颈,否则不必手动填充。如需极限优化,可考虑 LinkedTransferQueue 或 JCTools 等特化队列。
14.4 并发吞吐量粗略标尺
以下基于 8 核 CPU、JDK 8 的典型 JMH 测试结果(数量级参考,实际值取决于元素大小、GC 等):
| 队列 | 1 Producer + 1 Consumer (ops/s) | 4 Producer + 4 Consumer (ops/s) |
|---|---|---|
ArrayBlockingQueue(1000) | ~3,000,000 | ~1,200,000 |
LinkedBlockingQueue(1000) | ~5,000,000 | ~4,500,000 |
ConcurrentLinkedQueue | ~10,000,000 | ~9,000,000 |
SynchronousQueue (fair) | ~2,500,000 | ~1,800,000 |
LinkedTransferQueue | ~8,000,000 | ~7,500,000 |
注意:无界队列当容量无限膨胀时吞吐量更高,但有 OOM 风险,生产环境必须限制容量或做背压。
14.5 队列选型的并发维度决策建议
- 低竞争、简单场景:
ArrayBlockingQueue开销最低,内存占用固定。 - 高竞争、生产者与消费者对等:
LinkedBlockingQueue双锁基本满足多数互联网中间件需求。 - 极高竞争、非阻塞场景(如消息中间件内部传递):
ConcurrentLinkedQueue或LinkedTransferQueue,注意无界风险,配合限流。 - 需要严格的一对一任务交付:
SynchronousQueue+ 线程池按需伸缩(但需合理设置最大线程数)。 - 优先级/延迟调度:
PriorityBlockingQueue/DelayQueue,务必监控队列长度,防止无界增长。
结合模块 13 的选型决策树与本节并发分析,你应当能从容应对面试与工程中关于队列并发特性、性能差异的一切问询。