SynchronousQueue 详解

1. 概述

SynchronousQueuejava.util.concurrent 包中的一个特殊阻塞队列。与 ArrayBlockingQueueLinkedBlockingQueue 等传统阻塞队列不同,它内部没有任何容量 ------ 它不存储任何元素。每个插入操作(put / offer)必须等待一个对应的移除操作(take / poll),反之亦然。可以把它理解为一个 "汇合点" (handoff)或 "接力棒",生产者直接将元素交给消费者,元素永远不会在队列中停留。

核心特点

  • 容量为 0,size() 永远返回 0。
  • peek() 永远返回 null,因为没有任何元素可以被窥视。
  • 支持两种模式:公平模式 (使用 TransferQueue,FIFO)和 非公平模式 (使用 TransferStack,LIFO,默认)。
  • 核心算法基于 CAS 和无锁编程 ,不使用 ReentrantLock,性能极高。
  • puttake 会阻塞直到配对成功;非阻塞版本 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 阻塞直到有消费者 takepoll 接收该元素 InterruptedException(等待时被中断)、NullPointerException
offer(E e) e:元素 boolean:立即返回,成功配对返回 true,否则 false 不阻塞 NullPointerException
offer(E e, long timeout, TimeUnit unit) 元素、超时时间、时间单位 boolean:成功返回 true,超时未配对返回 false 等待指定时间(期间可被中断) InterruptedExceptionNullPointerException
take() E:接收到的元素 阻塞直到有生产者 putoffer 提供元素 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);
    }
    // ... 
}

根据构造器参数 fairtransferer 被初始化为 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);
            }
        }
    }
}

核心分支说明

  1. 栈空或模式相同 → 创建节点压栈,自旋或阻塞等待匹配。
  2. 模式互补且栈顶未在匹配中 → 尝试将当前节点设为 FULFILLING 模式,匹配栈顶节点,成功则弹出两个节点并返回元素。
  3. 栈顶正在匹配中 → 帮助完成匹配(无私行为),加速操作。

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 ,而是完全依赖 CASvolatile 变量实现无锁算法。例如:

  • headtail、节点的 nextmatchitem 等都用 volatile 修饰。
  • 通过 Unsafe 提供的 compareAndSwapObjectcompareAndSwapInt 等 CAS 操作保证原子性。
  • 这种无锁设计避免了上下文切换和锁竞争,在高并发场景下性能极优。

内存可见性volatile 保证了写操作对其他线程立即可见;CAS 具有 volatile 读写的内存语义。


4. 必要流程的 Mermaid 图

您说得对,虽然之前每个图后面都有简要描述,但为了更清晰、完整,我将为每一个图提供详细、独立的文字说明,确保不省略任何内容。以下为补充后的完整 4.1~4.7 部分。


4.1 类图

classDiagram class SynchronousQueue~E~ { -Transferer~E~ transferer +SynchronousQueue() +SynchronousQueue(boolean fair) +void put(E e) +E take() +boolean offer(E e) +E poll() +... } class Transferer~E~ { <> +E transfer(E e, boolean timed, long nanos) } class TransferStack~E~ { -SNode head +E transfer(E e, boolean timed, long nanos) } class TransferQueue~E~ { -QNode head -QNode tail +E transfer(E e, boolean timed, long nanos) } class SNode { -SNode next -SNode match -Thread waiter -Object item -int mode } class QNode { -QNode next -Object item -Thread waiter -boolean isData } SynchronousQueue --> Transferer Transferer <|-- TransferStack Transferer <|-- TransferQueue TransferStack --> SNode TransferQueue --> QNode

详细文字描述

此 UML 类图展示了 SynchronousQueue 的核心设计。SynchronousQueue 内部持有一个 Transferer 抽象引用,具体实现由构造函数的 fair 参数决定:若 fair = true 则使用 TransferQueue(公平模式),否则使用 TransferStack(非公平模式)。TransferStack 内部使用 SNode 作为栈节点,SNode 包含 next(栈指针)、match(配对节点引用)、waiter(阻塞线程)、item(数据或 null)以及 mode(REQUEST/DATA/FULFILLING)。TransferQueue 内部使用 QNode 作为队列节点,包含 nextitemwaiterisData(标识生产者还是消费者)。两个实现都实现了 transfer 方法,该方法同时处理生产者和消费者的逻辑。


4.2 非公平模式 TransferStack 结构图

graph LR subgraph 栈结构 head --> node2 node2 --> node1 node1 --> null end subgraph 节点详情 node2[ SNode
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 结构图

graph LR subgraph 队列结构 head --> q1 q1 --> q2 q2 --> q3 q3 --> null tail --> q3 end subgraph 节点详情 q1[ QNode
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 流程图(栈)

graph TD start([开始 transfer]) --> readHead[读取栈顶 h] readHead --> checkMode{栈空 或 h.mode == mode?} checkMode -- 是 --> sameMode[创建节点 s 并压栈] sameMode --> casHead{CAS 压栈成功?} casHead -- 否 --> readHead casHead -- 是 --> await[自旋/阻塞等待匹配] await --> matched{匹配成功?} matched -- 是 --> returnElem[返回元素] matched -- 否 --> returnNull[返回 null] checkMode -- 否 --> checkFulfilling{h 是否处于 FULFILLING?} checkFulfilling -- 否 --> fulfill[设为 FULFILLING 模式] fulfill --> tryMatch[尝试匹配栈顶 h] tryMatch --> matchSuccess{匹配成功?} matchSuccess -- 是 --> pop[弹出两个节点] pop --> returnElem2[返回元素] matchSuccess -- 否 --> clean[清理失败节点] --> readHead checkFulfilling -- 是 --> help[帮助完成匹配] --> readHead

详细文字描述

此流程图详细说明了非公平模式 TransferStack.transfer() 的执行路径。

  1. 模式相同或栈空 :读取栈顶 h,如果栈为空或栈顶模式与当前线程模式相同(同为生产者或同为消费者),则尝试创建节点并压栈(CAS)。压栈成功后,线程进入自旋+阻塞等待(awaitFulfill)。若等待过程中被匹配,则返回元素;若超时或中断,返回 null
  2. 模式互补且栈顶未处于 FULFILLING :说明当前线程与栈顶线程可以配对。当前线程将自己设置为 FULFILLING 模式(表示正在匹配),然后尝试匹配栈顶节点。匹配成功则 CAS 弹出两个节点,返回元素;失败则清理节点并重试。
  3. 栈顶处于 FULFILLING :说明有其他线程正在进行匹配,当前线程主动帮助完成匹配(例如推进栈顶指针),这是一种无私协作行为,有助于加快整体进度。
    整个流程是无锁的,通过 CAS 和自旋避免了阻塞开销。

4.5 公平模式 transfer 流程图(队列)

flowchart TD start([Start transfer]) --> readTail[Read tail t] readTail --> checkEmpty{Queue empty or t.isData == isData?} checkEmpty -- Yes --> enqueue[Create node and enqueue] enqueue --> await[Block] await --> afterAwake[Woken up] afterAwake --> matched{Matched?} matched -- Yes --> dequeue[Dequeue] dequeue --> returnElem[Return element] matched -- No --> returnNull[Return null] checkEmpty -- No --> checkHead[Check head.next = m] checkHead --> tryMatch[Try to match m] tryMatch --> success{CAS item exchange success?} success -- Yes --> dequeueHead[Dequeue m] dequeueHead --> wake[Unpark m.waiter] wake --> returnElem2[Return element] success -- No --> clean[Clean m] --> readTail

详细文字描述

此流程图详细说明了公平模式 TransferQueue.transfer() 的执行路径。

  1. 队列为空或队尾模式相同 :读取队尾 t,如果队列为空或队尾节点模式与当前线程相同,则创建节点并尝试入队(CAS 设置 t.next)。入队成功后,线程阻塞等待被匹配。当被唤醒后,如果成功匹配(即节点的 item 已被交换),则返回元素;否则(超时或取消)返回 null 并清理节点。
  2. 模式互补(队首节点与当前线程互补) :说明队首节点正在等待,当前线程直接与队首匹配。尝试用 CAS 交换数据(m.casItem(x, e))。若成功,队首出队,唤醒队首线程,返回元素;若失败(说明队首已被其他线程匹配或取消),则清理队首并重试。
    公平模式保证了等待最久的线程优先被服务,但每次匹配都需要操作队首和队尾,CAS 竞争比栈模式略多。

4.6 put 和 take 配对交互时序图(非公平模式)

sequenceDiagram participant P as 生产者线程 participant S as SynchronousQueue (TransferStack) participant C as 消费者线程 P->>S: put(e) activate P Note over S: 栈空,生产者节点压栈 S-->>P: 自旋+阻塞等待 Note over P: 线程挂起 C->>S: take() activate C Note over S: 发现栈顶为生产者节点,模式互补 S->>S: 创建 FULFILLING 节点,匹配栈顶 S->>P: 唤醒生产者线程 S->>C: 返回元素 e deactivate C S-->>P: 返回(put 完成) deactivate P

详细文字描述

该时序图展示了一个典型的非公平模式下的 put-take 配对过程。

  • 阶段 1 :生产者线程调用 put(e),此时栈为空,生产者创建 DATA 节点并压入栈顶,然后自旋一小段时间后阻塞(LockSupport.park),等待消费者。
  • 阶段 2 :消费者线程调用 take(),发现栈顶节点模式为 DATA,与自己的 REQUEST 模式互补。消费者创建一个 FULFILLING 节点(临时标记),尝试匹配栈顶节点。匹配成功后,通过 CAS 将栈顶节点的 match 指向自己,并唤醒生产者线程。
  • 阶段 3 :消费者从 take() 返回生产者提供的元素 e;生产者被唤醒后从 put() 返回。整个过程没有锁,只有 CAS 和线程唤醒。
    由于是 LIFO,后到的消费者匹配了先到的生产者,体现了非公平性。

4.7 超时处理流程图

graph TD start([Call offer]) --> calcDeadline[Compute deadline] calcDeadline --> loop[Enter loop] loop --> checkTimeout{Current time > deadline?} checkTimeout -- Yes --> returnFalse[Return false] checkTimeout -- No --> tryTransfer[Call transfer] tryTransfer --> transferResult{Transfer result?} transferResult -- non-null --> returnTrue[Return true] transferResult -- null --> checkRemain{Remaining time > 0?} checkRemain -- Yes --> loop checkRemain -- No --> returnFalse style calcDeadline fill:#ccf,stroke:#333 style returnFalse fill:#f99,stroke:#333 style returnTrue fill:#9f9,stroke:#333

详细文字描述

此流程图展示了带超时的方法(offer(e, timeout, unit)poll(timeout, unit))的处理逻辑。

  1. 计算截止时间 :根据当前时间和超时时长计算出绝对截止时间 deadline
  2. 进入循环 :每次循环先检查当前时间是否超过 deadline,若超过则立即返回 false(或 null)。
  3. 调用 transfer :调用 transferer.transfer(e, timed=true, nanos),该方法会在内部根据剩余时间自旋或阻塞。
    • 如果 transfer 返回非 null(成功配对),则返回 true(或元素)。
    • 如果返回 null(未配对且未超时),则检查剩余时间,若还有时间则继续循环尝试;若剩余时间 ≤ 0 则返回 false
  4. 超时控制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 使用建议

  1. 线程池任务直接传递SynchronousQueue 最适合作为 ThreadPoolExecutor 的任务队列,实现 newCachedThreadPool 的弹性伸缩。
  2. 需要严格的生产者-消费者同步:例如每个生产的数据必须立即被消费,不允许积压。
  3. 根据公平性需求选择模式:默认非公平模式提供更高吞吐量;如果业务要求严格的 FIFO 顺序,使用公平模式。
  4. 避免依赖 size()peek()drainTo():它们不会返回有意义的结果。
  5. 注意资源风险 :在 newCachedThreadPool 中,生产者过快可能导致线程数爆炸,需监控或设置线程池上限。

9.3 与其他队列的协同使用

在实际系统中,SynchronousQueue 常与有界队列配合使用:例如,使用 SynchronousQueue 作为直接 handoff 队列,当任务无法立即被消费时,降级到 LinkedBlockingQueue 做缓冲。这种设计可以实现高吞吐同时防止任务丢失。

9.4 学习指引与预告

通过本篇文章,你已经掌握了 SynchronousQueue 的核心原理、源码实现、性能特征及应用场景。这是阻塞队列系列中设计最精巧、性能最高的一个,其无锁算法值得深入研读。

下一讲预告LinkedTransferQueue ------ 它结合了 SynchronousQueue 的 handoff 特性和 LinkedBlockingQueue 的缓冲能力,提供 transfer 方法(阻塞直到元素被消费),同时支持异步的 put 和批量操作。敬请期待阻塞队列系列第六篇!


本文基于 JDK 8 源码编写,所有代码示例均可在 JDK 8+ 环境下编译运行。

相关推荐
wuyikeer2 小时前
Spring Boot 经典九设计模式全览
java·spring boot·设计模式
努力努力再努力wz2 小时前
【Linux网络系列】深入理解 I/O 多路复用:从 select 痛点到 poll 高并发服务器落地,基于 Poll、智能指针与非阻塞 I/O与线程池手写一个高性能 HTTP 服务器!(附源码)
java·linux·运维·服务器·c语言·c++·python
努力努力再努力wz2 小时前
【Linux网络系列】万字硬核解析网络层核心:IP协议到IP 分片重组、NAT技术及 RIP/OSPF 动态路由全景
java·linux·运维·服务器·数据结构·c++·python
LaLaLa_OvO2 小时前
mybatis 引用静态常量
java·mybatis
Han_han9192 小时前
常用API:
java·开发语言
小锋java12342 小时前
LangChain4j 来了,Java AI智能体开发再次起飞。。。
java·人工智能·后端
敖正炀2 小时前
BlockingQueue 详解
java
likerhood2 小时前
java中的return this、链式编程和Builder模式
java·开发语言
spring2997922 小时前
Spring Boot 实战篇(四):实现用户登录与注册功能
java·spring boot·后端