1. 概述
SynchronousQueue 是 java.util.concurrent 包中的一个特殊阻塞队列。与 ArrayBlockingQueue、LinkedBlockingQueue 等传统阻塞队列不同,它内部没有任何容量 ------ 它不存储任何元素。每个插入操作(put / offer)必须等待一个对应的移除操作(take / poll),反之亦然。可以把它理解为一个 "汇合点" (handoff)或 "接力棒",生产者直接将元素交给消费者,元素永远不会在队列中停留。
核心特点:
- 容量为 0,
size()永远返回 0。 peek()永远返回null,因为没有任何元素可以被窥视。- 支持两种模式:公平模式 (使用
TransferQueue,FIFO)和 非公平模式 (使用TransferStack,LIFO,默认)。 - 核心算法基于 CAS 和无锁编程 ,不使用
ReentrantLock,性能极高。 put和take会阻塞直到配对成功;非阻塞版本offer/poll立即返回;支持超时等待。drainTo方法无效,永远返回 0。
典型应用场景:
- 线程池的任务队列 :
Executors.newCachedThreadPool()内部使用SynchronousQueue。每当有任务提交时,如果当前没有空闲线程,线程池会创建新线程来处理;如果线程数达到上限,则拒绝任务。这实现了"直接 handoff"模式,避免任务在队列中堆积。 - 生产者-消费者同步解耦:要求生产者必须等待消费者处理完上一个数据才能生产下一个,实现严格同步。
- 数据交换 :两个线程之间传递数据,类似于
Exchanger,但支持多生产者多消费者。
与其他阻塞队列的根本区别:
ArrayBlockingQueue/LinkedBlockingQueue有实际容量,用于缓冲;SynchronousQueue无容量,用于直接传递。PriorityBlockingQueue按优先级排序;DelayQueue按延迟时间出队;SynchronousQueue只关心"配对"。SynchronousQueue更像是线程间的握手协议,而不是数据容器。
与 Exchanger 的异同:
- 相同点:都可用于两个线程交换数据,都提供同步点。
- 不同点:
Exchanger专门用于 两个线程 交换数据(一对一的对称交换);SynchronousQueue支持 多个生产者、多个消费者,生产者将数据给消费者(单向传递),并且可以通过公平/非公平模式控制匹配顺序。
2. 核心方法说明
下表列出了 SynchronousQueue 的主要方法及其行为(基于 JDK 8):
| 方法签名 | 参数 | 返回值 | 阻塞行为 | 异常 |
|---|---|---|---|---|
SynchronousQueue() |
无 | 构造器,创建非公平模式(默认) | 无 | 无 |
SynchronousQueue(boolean fair) |
fair:true 公平模式(队列),false 非公平(栈) |
构造器 | 无 | 无 |
put(E e) |
e:要插入的元素(不能为 null) |
void |
阻塞直到有消费者 take 或 poll 接收该元素 |
InterruptedException(等待时被中断)、NullPointerException |
offer(E e) |
e:元素 |
boolean:立即返回,成功配对返回 true,否则 false |
不阻塞 | NullPointerException |
offer(E e, long timeout, TimeUnit unit) |
元素、超时时间、时间单位 | boolean:成功返回 true,超时未配对返回 false |
等待指定时间(期间可被中断) | InterruptedException、NullPointerException |
take() |
无 | E:接收到的元素 |
阻塞直到有生产者 put 或 offer 提供元素 |
InterruptedException |
poll() |
无 | E:立即返回,成功配对返回元素,否则 null |
不阻塞 | 无 |
poll(long timeout, TimeUnit unit) |
超时时间、时间单位 | E:元素,超时未配对返回 null |
等待指定时间 | InterruptedException |
peek() |
无 | E:始终返回 null |
不阻塞 | 无 |
size() |
无 | int:始终返回 0(不代表实际等待线程数) |
无 | 无 |
remainingCapacity() |
无 | int:始终返回 0 |
无 | 无 |
drainTo(Collection<? super E> c) |
目标集合 | int:始终返回 0(无法转移任何元素) |
无 | 无 |
drainTo(Collection<? super E> c, int maxElements) |
目标集合、最大转移数 | int:始终返回 0 |
无 | 无 |
isEmpty() |
无 | boolean:始终返回 true |
无 | 无 |
注意 :虽然
size()返回 0,但可能有许多线程在等待配对,无法通过size()获知。如果需要统计等待线程数,需要额外的计数器。
3. 核心原理与源码分析(基于 JDK 8)
3.1 数据结构与核心字段
SynchronousQueue 的核心是一个 Transferer 抽象类,它定义了 transfer 方法,同时处理生产者和消费者的逻辑:
java
public class SynchronousQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// 核心字段:负责实际的数据传递
private transient volatile Transferer<E> transferer;
// 抽象类
abstract static class Transferer<E> {
abstract E transfer(E e, boolean timed, long nanos);
}
// ...
}
根据构造器参数 fair,transferer 被初始化为 TransferQueue(公平模式)或 TransferStack(非公平模式):
java
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
非公平模式:TransferStack
- 使用 栈 结构(LIFO),后到达的线程可能先被匹配。
- 节点类型
SNode,包含mode字段,取值为REQUEST(消费者)、DATA(生产者)、FULFILLING(匹配中)。
公平模式:TransferQueue
- 使用 队列 结构(FIFO),先到达的线程先被匹配。
- 节点类型
QNode,包含isData标识是生产者还是消费者。
3.2 TransferStack 源码分析(非公平模式)
3.2.1 节点定义 SNode
java
static final class SNode {
volatile SNode next; // 栈中的下一个节点
volatile SNode match; // 与当前节点配对的节点
volatile Thread waiter; // 等待的线程
Object item; // 生产者存储元素,消费者为 null
int mode; // 模式: REQUEST(0), DATA(1), FULFILLING(2)
}
mode含义:REQUEST = 0:消费者等待数据。DATA = 1:生产者等待被消费。FULFILLING = 2:当前节点正在与栈顶节点进行匹配(用于防止多线程同时匹配)。
3.2.2 transfer 方法核心逻辑
java
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) { // 不等待且超时
if (h != null && h.isCancelled())
casHead(h, h.next); // 清理已取消节点
else
return null;
} else if (casHead(h, s = snode(s, e, h, mode))) {
// 创建节点并压入栈顶
SNode m = awaitFulfill(s, timed, nanos); // 等待匹配
if (m == s) { // 等待过程中被取消
clean(s);
return null;
}
if ((h = head) != null && h.next == s)
casHead(h, s.next); // 帮助推进 head
return (E) ((mode == REQUEST) ? m.item : s.item);
}
} else if (!isFulfilling(h.mode)) { // 模式互补且栈顶未处于匹配状态
if (h.isCancelled()) // 栈顶已取消
casHead(h, h.next);
else if (casHead(h, s = snode(s, e, h, FULFILLING | mode))) {
// 将当前节点设为 FULFILLING 模式,尝试匹配栈顶
for (;;) {
SNode m = s.next; // m 是原来的栈顶
if (m == null) { // 已被其他线程匹配
casHead(s, null);
s = null;
break;
}
SNode mn = m.next;
if (m.tryMatch(s)) { // 尝试匹配
casHead(s, mn); // 弹出 s 和 m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // 匹配失败
s.casNext(m, mn); // 跳过失败节点
}
}
} else { // 栈顶正处于 FULFILLING 状态,帮助完成匹配
SNode m = h.next;
if (m == null) // 没有其他节点
casHead(h, null);
else {
SNode mn = m.next;
if (h.tryMatch(m)) // 帮助匹配
casHead(h, mn);
else
h.casNext(m, mn);
}
}
}
}
核心分支说明:
- 栈空或模式相同 → 创建节点压栈,自旋或阻塞等待匹配。
- 模式互补且栈顶未在匹配中 → 尝试将当前节点设为
FULFILLING模式,匹配栈顶节点,成功则弹出两个节点并返回元素。 - 栈顶正在匹配中 → 帮助完成匹配(无私行为),加速操作。
3.2.3 自旋与阻塞策略
在 awaitFulfill 方法中,线程在阻塞前会先自旋一定次数:
java
static final int spinForTimeoutThreshold = 1000; // 纳秒
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
int spins = (shouldSpin(s) ? (timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
if (w.isInterrupted())
s.tryCancel();
SNode m = s.match;
if (m != null)
return m;
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
if (spins > 0) {
spins = shouldSpin(s) ? (spins - 1) : 0;
} else if (s.waiter == null)
s.waiter = w;
else if (!timed)
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}
- 自旋 :当节点位于栈顶时,
shouldSpin返回true,会先自旋(最多maxUntimedSpins次,根据 CPU 数量动态调整),避免立即进入阻塞,提高短时等待的响应速度。 - 阻塞 :自旋结束后仍未匹配,则调用
LockSupport.park/parkNanos挂起线程。 - 超时 :超时后调用
tryCancel将节点match指向自身,标记为取消。
3.3 TransferQueue 源码分析(公平模式)
3.3.1 节点定义 QNode
java
static final class QNode {
volatile QNode next; // 队列中的下一个节点
volatile Object item; // 生产者的数据,消费者的 null 占位符
volatile Thread waiter; // 等待的线程
final boolean isData; // true 生产者,false 消费者
}
3.3.2 transfer 方法核心逻辑
java
E transfer(E e, boolean timed, long nanos) {
QNode s = null;
boolean isData = (e != null);
for (;;) {
QNode t = tail;
QNode h = head;
if (t == null || h == null) // 未初始化
continue;
if (h == t || t.isData == isData) { // 队列为空或模式相同
QNode tn = t.next;
if (t != tail) // 不一致读,重试
continue;
if (tn != null) { // 帮助推进 tail
advanceTail(t, tn);
continue;
}
if (timed && nanos <= 0) // 超时且不等待
return null;
if (s == null)
s = new QNode(e, isData);
if (!t.casNext(null, s)) // 入队
continue;
advanceTail(t, s); // 移动 tail
Object x = awaitFulfill(s, e, timed, nanos); // 等待匹配
if (x == s) { // 取消
clean(s);
return null;
}
if (!s.isOffList()) { // 未出队则尝试出队
advanceHead(h, s.next);
if (s.waiter != null)
s.waiter = null;
}
return (x != null) ? (E)x : e;
} else { // 模式互补,进行匹配
QNode m = h.next; // 队首节点
if (t != tail || m == null || h != head)
continue;
Object x = m.item;
if (isData == (x != null) || // 模式不匹配
x == m || // 节点已取消
!m.casItem(x, e)) { // 尝试交换数据
advanceHead(h, m); // 出队并重试
continue;
}
advanceHead(h, m); // 成功匹配,队首出队
LockSupport.unpark(m.waiter); // 唤醒等待线程
return (x != null) ? (E)x : e;
}
}
}
公平模式核心思想:
- 所有等待的生产者和消费者按 FIFO 顺序排队。
- 新到达的线程检查队列是否为空或队尾节点模式是否与自己相同 → 若是,则入队并阻塞。
- 若队首节点模式互补 → 进行匹配,队首出队,唤醒队首线程,返回数据。
- 这种设计保证了公平性,但可能降低吞吐量(因为要严格排队)。
3.4 公平与非公平模式对比
| 特性 | TransferStack (非公平) | TransferQueue (公平) |
|---|---|---|
| 数据结构 | 栈(LIFO) | 队列(FIFO) |
| 匹配顺序 | 后到先匹配,可能导致饥饿 | 先到先匹配,无饥饿 |
| 性能 | 更高(局部性好,自旋更有效) | 较低(严格排队增加上下文切换) |
| 适用场景 | 高吞吐量,对顺序无要求 | 需要公平性,避免线程饥饿 |
3.5 锁与并发机制
SynchronousQueue 没有使用任何 ReentrantLock ,而是完全依赖 CAS 和 volatile 变量实现无锁算法。例如:
head、tail、节点的next、match、item等都用volatile修饰。- 通过
Unsafe提供的compareAndSwapObject、compareAndSwapInt等 CAS 操作保证原子性。 - 这种无锁设计避免了上下文切换和锁竞争,在高并发场景下性能极优。
内存可见性 :volatile 保证了写操作对其他线程立即可见;CAS 具有 volatile 读写的内存语义。
4. 必要流程的 Mermaid 图
您说得对,虽然之前每个图后面都有简要描述,但为了更清晰、完整,我将为每一个图提供详细、独立的文字说明,确保不省略任何内容。以下为补充后的完整 4.1~4.7 部分。
4.1 类图
详细文字描述 :
此 UML 类图展示了 SynchronousQueue 的核心设计。SynchronousQueue 内部持有一个 Transferer 抽象引用,具体实现由构造函数的 fair 参数决定:若 fair = true 则使用 TransferQueue(公平模式),否则使用 TransferStack(非公平模式)。TransferStack 内部使用 SNode 作为栈节点,SNode 包含 next(栈指针)、match(配对节点引用)、waiter(阻塞线程)、item(数据或 null)以及 mode(REQUEST/DATA/FULFILLING)。TransferQueue 内部使用 QNode 作为队列节点,包含 next、item、waiter 和 isData(标识生产者还是消费者)。两个实现都实现了 transfer 方法,该方法同时处理生产者和消费者的逻辑。
4.2 非公平模式 TransferStack 结构图
mode=DATA
item=E1
match=null
waiter=Thread2 ] node1[ SNode
mode=REQUEST
item=null
match=null
waiter=Thread1 ] end style node2 fill:#f9f,stroke:#333,stroke-width:2px style node1 fill:#bbf,stroke:#333,stroke-width:2px
该图描绘了非公平模式下的栈结构(LIFO)。图中栈顶(head)指向一个 DATA 节点(生产者),它携带数据 E1,等待被消费者匹配。栈的下一个节点是一个 REQUEST 节点(消费者),等待生产者提供数据。每个节点都持有 match 指针(初始为 null,配对后会指向匹配的节点)以及 waiter 线程引用。新到达的线程(无论是生产者还是消费者)会先检查栈顶模式:如果栈空或模式相同,则新节点成为新的栈顶(压栈);如果模式互补(如栈顶是 DATA,新来的是 REQUEST),则尝试匹配,匹配成功后两个节点出栈,match 指针互相指向,并唤醒等待线程。LIFO 特性使得后到达的线程优先被匹配,从而提高吞吐量。
4.3 公平模式 TransferQueue 结构图
isData=true
item=E1
waiter=Thread1 ] q2[ QNode
isData=false
item=null
waiter=Thread2 ] q3[ QNode
isData=true
item=E2
waiter=Thread3 ] end style head fill:#ccc,stroke:#333 style tail fill:#ccc,stroke:#333
详细文字描述 :
该图描绘了公平模式下的队列结构(FIFO)。head 指向队列的第一个有效节点(通常是虚拟头节点的下一个),tail 指向最后一个节点。图中队列包含三个等待节点:第一个是生产者(isData=true,携带数据 E1),第二个是消费者(isData=false,等待数据),第三个是生产者(isData=true,携带数据 E2)。在公平模式下,新到达的线程会检查队尾节点的模式:如果队尾模式相同(或队列为空),则新节点入队成为新队尾,并阻塞等待;如果队首节点模式互补(如队首是生产者,新到达的是消费者),则立即进行匹配,队首节点出队,数据从生产者交给消费者,并唤醒队首线程。FIFO 保证了先到先服务,避免饥饿,但可能降低吞吐量。
4.4 非公平模式 transfer 流程图(栈)
详细文字描述 :
此流程图详细说明了非公平模式 TransferStack.transfer() 的执行路径。
- 模式相同或栈空 :读取栈顶
h,如果栈为空或栈顶模式与当前线程模式相同(同为生产者或同为消费者),则尝试创建节点并压栈(CAS)。压栈成功后,线程进入自旋+阻塞等待(awaitFulfill)。若等待过程中被匹配,则返回元素;若超时或中断,返回null。 - 模式互补且栈顶未处于 FULFILLING :说明当前线程与栈顶线程可以配对。当前线程将自己设置为
FULFILLING模式(表示正在匹配),然后尝试匹配栈顶节点。匹配成功则 CAS 弹出两个节点,返回元素;失败则清理节点并重试。 - 栈顶处于 FULFILLING :说明有其他线程正在进行匹配,当前线程主动帮助完成匹配(例如推进栈顶指针),这是一种无私协作行为,有助于加快整体进度。
整个流程是无锁的,通过 CAS 和自旋避免了阻塞开销。
4.5 公平模式 transfer 流程图(队列)
详细文字描述 :
此流程图详细说明了公平模式 TransferQueue.transfer() 的执行路径。
- 队列为空或队尾模式相同 :读取队尾
t,如果队列为空或队尾节点模式与当前线程相同,则创建节点并尝试入队(CAS 设置t.next)。入队成功后,线程阻塞等待被匹配。当被唤醒后,如果成功匹配(即节点的item已被交换),则返回元素;否则(超时或取消)返回null并清理节点。 - 模式互补(队首节点与当前线程互补) :说明队首节点正在等待,当前线程直接与队首匹配。尝试用 CAS 交换数据(
m.casItem(x, e))。若成功,队首出队,唤醒队首线程,返回元素;若失败(说明队首已被其他线程匹配或取消),则清理队首并重试。
公平模式保证了等待最久的线程优先被服务,但每次匹配都需要操作队首和队尾,CAS 竞争比栈模式略多。
4.6 put 和 take 配对交互时序图(非公平模式)
详细文字描述 :
该时序图展示了一个典型的非公平模式下的 put-take 配对过程。
- 阶段 1 :生产者线程调用
put(e),此时栈为空,生产者创建DATA节点并压入栈顶,然后自旋一小段时间后阻塞(LockSupport.park),等待消费者。 - 阶段 2 :消费者线程调用
take(),发现栈顶节点模式为DATA,与自己的REQUEST模式互补。消费者创建一个FULFILLING节点(临时标记),尝试匹配栈顶节点。匹配成功后,通过 CAS 将栈顶节点的match指向自己,并唤醒生产者线程。 - 阶段 3 :消费者从
take()返回生产者提供的元素e;生产者被唤醒后从put()返回。整个过程没有锁,只有 CAS 和线程唤醒。
由于是 LIFO,后到的消费者匹配了先到的生产者,体现了非公平性。
4.7 超时处理流程图
详细文字描述 :
此流程图展示了带超时的方法(offer(e, timeout, unit) 或 poll(timeout, unit))的处理逻辑。
- 计算截止时间 :根据当前时间和超时时长计算出绝对截止时间
deadline。 - 进入循环 :每次循环先检查当前时间是否超过
deadline,若超过则立即返回false(或null)。 - 调用 transfer :调用
transferer.transfer(e, timed=true, nanos),该方法会在内部根据剩余时间自旋或阻塞。- 如果
transfer返回非null(成功配对),则返回true(或元素)。 - 如果返回
null(未配对且未超时),则检查剩余时间,若还有时间则继续循环尝试;若剩余时间 ≤ 0 则返回false。
- 如果
- 超时控制 :
transfer内部会使用LockSupport.parkNanos并处理虚假唤醒,每次醒来都会重新计算剩余时间,确保总等待时间不超过用户指定的值。
此机制保证了调用者可以在指定时间内等待配对,超时后立即返回,避免无限阻塞。
5. 实际应用场景与代码举例(JDK 8 兼容)
5.1 线程池中的 SynchronousQueue
Executors.newCachedThreadPool() 内部使用 SynchronousQueue 作为工作队列。
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CachedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 1; i <= 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
// 输出可以看到线程被重复使用或新建(取决于并发)
}
}
5.2 生产者-消费者手递手模型
java
import java.util.concurrent.SynchronousQueue;
public class HandoffDemo {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue<>();
// 消费者线程
Thread consumer = new Thread(() -> {
try {
String data = queue.take();
System.out.println("消费者收到: " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 生产者线程
Thread producer = new Thread(() -> {
try {
System.out.println("生产者准备发送数据...");
queue.put("Hello SynchronousQueue");
System.out.println("生产者已发送,消费者已接收");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
consumer.start();
Thread.sleep(100); // 确保消费者先等待
producer.start();
producer.join();
consumer.join();
}
}
5.3 非阻塞 offer/poll 使用
java
import java.util.concurrent.SynchronousQueue;
public class NonBlockingDemo {
public static void main(String[] args) {
SynchronousQueue<String> queue = new SynchronousQueue<>();
// 生产者尝试 offer,此时无消费者,立即失败
boolean offered = queue.offer("data");
System.out.println("offer 结果: " + offered); // false
// 消费者尝试 poll,无生产者,立即返回 null
String polled = queue.poll();
System.out.println("poll 结果: " + polled); // null
}
}
5.4 超时尝试
java
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
public class TimeoutDemo {
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue<>();
Thread producer = new Thread(() -> {
try {
boolean success = queue.offer("timeout-data", 2, TimeUnit.SECONDS);
if (success) {
System.out.println("生产者: 数据被消费");
} else {
System.out.println("生产者: 超时,无消费者");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
// 消费者等待 1 秒才开始,导致生产者超时
Thread.sleep(1000);
// 如果取消下面注释,生产者会成功
// String result = queue.poll();
// System.out.println("消费者: " + result);
producer.join();
}
}
5.5 公平与非公平对比
java
import java.util.concurrent.SynchronousQueue;
public class FairVsNonFairDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 非公平模式 (默认) ===");
testQueue(new SynchronousQueue<String>(), false);
Thread.sleep(2000);
System.out.println("\n=== 公平模式 ===");
testQueue(new SynchronousQueue<String>(true), true);
}
private static void testQueue(SynchronousQueue<String> queue, boolean fair) throws InterruptedException {
// 先启动 3 个消费者线程,依次等待
for (int i = 1; i <= 3; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("消费者" + id + " 开始等待...");
String data = queue.take();
System.out.println("消费者" + id + " 获得数据: " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
Thread.sleep(100); // 确保按顺序启动
}
Thread.sleep(500);
// 然后启动 3 个生产者,依次发送数据
for (int i = 1; i <= 3; i++) {
final int id = i;
new Thread(() -> {
try {
queue.put("数据" + id);
System.out.println("生产者" + id + " 已发送");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
Thread.sleep(100);
}
Thread.sleep(1000);
}
}
预期结果(非公平模式):生产者可能不会按消费者启动顺序匹配,可能出现"后到先得"。公平模式下,消费者1 总是获得数据1,消费者2 获得数据2,依此类推。
6. 吞吐量与性能分析
6.1 无锁设计的性能优势
- 无上下文切换:使用 CAS 自旋,在低竞争情况下,线程不会进入阻塞,避免了操作系统级的线程调度开销。
- 无锁竞争 :传统锁(如
ReentrantLock)在高并发下会引起锁争用和上下文切换;SynchronousQueue通过无锁算法,多个线程可以同时进行 CAS 操作,失败的重试也很快。 - 内存效率:不存储元素,节点仅在线程等待时存在,配对后立即释放,GC 压力小。
6.2 栈模式 vs 队列模式性能差异
- 栈模式(LIFO) :
- 优势 :最近到达的线程在栈顶,自旋时可以更快被匹配;减少了平均等待时间;吞吐量更高(JDK 官方文档也指出非公平模式通常比公平模式有更高的吞吐量)。
- 劣势:可能导致某些线程长时间得不到服务(饥饿),但通常可以接受。
- 队列模式(FIFO) :
- 优势:公平,线程按到达顺序匹配,无饥饿。
- 劣势:需要维护队列头和尾,多线程 CAS 竞争更激烈;阻塞和唤醒顺序严格,平均等待时间较长,吞吐量较低。
6.3 自旋策略分析
spinForTimeoutThreshold = 1000纳秒:超时小于此阈值时,不进入阻塞,而是自旋等待,因为阻塞的开销大于自旋。- 动态自旋次数:
maxUntimedSpins根据 CPU 数量调整(32 或 0),多核 CPU 下自旋次数更多,提高短时等待的响应速度。 - 代价:自旋会占用 CPU,如果等待时间较长,会浪费 CPU 资源;但设计上自旋仅用于短时间期望匹配的场景。
6.4 与其他队列对比
| 队列 | 容量 | 锁机制 | 内存占用 | 吞吐量(典型场景) |
|---|---|---|---|---|
SynchronousQueue |
0 | 无锁 CAS | 极小 | 极高(直接传递) |
LinkedBlockingQueue |
可选有界/无界 | 双锁(takeLock/putLock) | 存储元素,较高 | 较高(缓冲场景) |
ArrayBlockingQueue |
固定有界 | 单锁 | 数组预分配 | 中高 |
PriorityBlockingQueue |
无界 | 单锁 | 存储元素 + 堆 | 中 |
DelayQueue |
无界 | 单锁 | 存储元素 | 中低(需延迟检查) |
性能调优建议:
- 高吞吐任务传递:使用非公平模式(默认),避免公平性带来的开销。
- 避免过小的超时值:超时时间过短会导致频繁失败重试,浪费 CPU。
- 合理控制线程数量 :
SynchronousQueue本身不存储任务,若生产者远快于消费者,会导致大量线程阻塞(在TransferStack中积累节点),可能引起内存压力。在newCachedThreadPool中,线程数无上限,需要注意资源耗尽风险。
7. 注意事项与常见陷阱
7.1 size() 永远返回 0
原因 :SynchronousQueue 不存储任何元素,size() 硬编码返回 0。不能依赖 size() 判断是否有等待线程。如果需要知道等待线程数,需要自己维护计数器。
7.2 peek() 永远返回 null
原因 :队列中没有元素可窥视。不能用于检查是否有生产者正在等待。
7.3 drainTo 无效
原因 :drainTo 的设计目的是批量转移元素,但 SynchronousQueue 没有元素可以转移,方法直接返回 0。不要期望用 drainTo 获取多个元素。
7.4 put 和 take 必须成对出现
原因 :这是 SynchronousQueue 的核心约束。如果只有生产者调用 put 而没有消费者调用 take,生产者会永久阻塞(除非线程被中断)。反之亦然。
7.5 offer 非阻塞版本可能失败
原因 :offer(e) 是立即返回的,如果当前没有等待的消费者,直接返回 false。容易误以为是队列满了,实际是没有消费者。在非阻塞场景下需要检查返回值。
7.6 公平模式可能降低吞吐量
原因 :公平模式使用队列,严格的 FIFO 顺序增加了锁竞争和线程唤醒开销。如果需要高吞吐量,优先使用非公平模式。
7.7 内存可见性已保证
SynchronousQueue 的无锁算法正确使用了 volatile 和 CAS,保证了线程间的内存可见性。普通开发者无需额外同步。
7.8 与 Exchanger 的区别
Exchanger:一对一交换,两个线程互相等待对方到达同步点,然后交换数据。SynchronousQueue:多对多,生产者将数据给任意等待的消费者。不要混淆。
7.9 线程池使用时的资源风险
Executors.newCachedThreadPool() 使用 SynchronousQueue,当生产者提交任务的速度远高于消费者处理速度时,线程池会不断创建新线程,直到 Integer.MAX_VALUE,可能导致系统资源耗尽(内存、文件描述符等)。在生产环境中应设置线程池上限。
8. 与其他阻塞队列的对比总结
| 维度 | SynchronousQueue | ArrayBlockingQueue | LinkedBlockingQueue | PriorityBlockingQueue | DelayQueue |
|---|---|---|---|---|---|
| 容量 | 0(无容量) | 固定有界(构造时指定) | 可选有界(默认无界) | 无界(实际受内存限制) | 无界 |
| 存储结构 | 无(不存储元素) | 数组(循环) | 链表 | 二叉堆(数组实现) | 二叉堆(存储 Delayed 元素) |
| 锁机制 | 无锁 CAS + 自旋 | 单锁(ReentrantLock) | 双锁(takeLock/putLock) | 单锁(ReentrantLock) | 单锁(ReentrantLock) |
| 公平性支持 | 支持(公平/非公平两种模式) | 支持(构造参数 fair) | 不支持(默认非公平) | 不支持 | 不支持(按延迟时间出队) |
| 阻塞生产者 | 是(put 阻塞直到消费者就绪) | 是(队列满时阻塞) | 是(有界且满时阻塞) | 否(无界,不会满) | 否(无界,不会满) |
| 阻塞消费者 | 是(take 阻塞直到生产者就绪) | 是(队列空时阻塞) | 是(队列空时阻塞) | 是(队列空时阻塞) | 是(队列空或头部未到期时阻塞) |
| 典型应用场景 | 线程池任务直接传递(handoff),生产者-消费者严格同步 | 固定大小的缓冲区,如生产者-消费者模式 | 高并发队列,如工作队列 | 优先级任务调度 | 延迟任务执行(如缓存过期、定时任务) |
| 吞吐量特征 | 极高(无锁,直接传递) | 中高(有界,单锁) | 高(双锁分离) | 中(需维护堆) | 中低(需延迟检查) |
| 内存占用 | 极小(无元素存储) | 预分配数组,固定 | 动态链表节点 | 动态数组扩容 | 动态数组扩容 |
9. 总结与学习指引
9.1 SynchronousQueue 核心特点总结
- 零容量:不存储任何元素,是纯粹的手递手同步工具。
- 手递手机制 :每个
put必须等待一个take,反之亦然。 - 无锁设计:基于 CAS 和自旋,性能极高。
- 双模式 :公平(
TransferQueue)和非公平(TransferStack,默认),可根据需求选择。 - 特殊方法行为 :
size()、peek()、drainTo()均无实际意义。
9.2 使用建议
- 线程池任务直接传递 :
SynchronousQueue最适合作为ThreadPoolExecutor的任务队列,实现newCachedThreadPool的弹性伸缩。 - 需要严格的生产者-消费者同步:例如每个生产的数据必须立即被消费,不允许积压。
- 根据公平性需求选择模式:默认非公平模式提供更高吞吐量;如果业务要求严格的 FIFO 顺序,使用公平模式。
- 避免依赖
size()、peek()、drainTo():它们不会返回有意义的结果。 - 注意资源风险 :在
newCachedThreadPool中,生产者过快可能导致线程数爆炸,需监控或设置线程池上限。
9.3 与其他队列的协同使用
在实际系统中,SynchronousQueue 常与有界队列配合使用:例如,使用 SynchronousQueue 作为直接 handoff 队列,当任务无法立即被消费时,降级到 LinkedBlockingQueue 做缓冲。这种设计可以实现高吞吐同时防止任务丢失。
9.4 学习指引与预告
通过本篇文章,你已经掌握了 SynchronousQueue 的核心原理、源码实现、性能特征及应用场景。这是阻塞队列系列中设计最精巧、性能最高的一个,其无锁算法值得深入研读。
下一讲预告 :LinkedTransferQueue ------ 它结合了 SynchronousQueue 的 handoff 特性和 LinkedBlockingQueue 的缓冲能力,提供 transfer 方法(阻塞直到元素被消费),同时支持异步的 put 和批量操作。敬请期待阻塞队列系列第六篇!
本文基于 JDK 8 源码编写,所有代码示例均可在 JDK 8+ 环境下编译运行。