阻塞队列-4-LinkedTransferQueue 详解

16 阅读25分钟

1. 概述

LinkedTransferQueue 是 Java 并发包中一个强大且复杂的阻塞队列实现。它实现了 TransferQueue 接口,融合了 SynchronousQueue直接传递(handoff)特性和 LinkedBlockingQueue缓冲能力。简单来说,它既允许生产者将元素放入队列供消费者后续获取,也支持生产者阻塞等待消费者直接取走元素,而不经过队列的“中间缓存”。

核心特点

  • 无界队列:基于链表实现,理论上容量受限于内存,永远不会因容量满而阻塞生产者(put 永远不会阻塞)。
  • 支持 transfer 语义:生产者可以通过 transfer(e) 阻塞等待,直到某个消费者取走该元素;tryTransfer(e) 可尝试立即传递,失败则立即返回而不入队。
  • 无锁算法:内部大量使用 CAS(Compare-And-Swap)操作,避免使用显式锁(如 ReentrantLock),在高并发场景下提供更好的性能。
  • 公平 FIFO 匹配:匹配等待线程时,总是从队列头部(head)开始扫描,保证等待时间最长的线程先被服务,避免饥饿。
  • 双链表结构:JDK 8 中 Node 包含 prevnext 指针,便于快速移除已匹配的节点,相比 JDK 7 的单链表有较大改进。

典型应用场景

  • 生产者需要确认消费者已接收:例如消息中间件中,发送端要求确保消息被接收端处理后才继续后续操作。
  • 高吞吐量的数据交换:无锁设计减少了线程上下文切换,适合多生产者多消费者环境。
  • 实现轻量级的 Exchanger:利用 transfer 的配对行为,两个线程可以安全地交换数据。

与其他队列的主要区别

队列容量锁机制是否支持 transfer特点
LinkedTransferQueue无界无锁(CAS)可缓冲可 handoff,公平 FIFO,高吞吐
LinkedBlockingQueue可选有界(默认无界)双锁(takeLock/putLock)有锁,size O(1),适合传统生产者-消费者缓冲
SynchronousQueue0(零容量)无锁(CAS)是(本质就是 handoff)每一个 put 必须等待一个 take,无缓冲,常用于线程池
ArrayBlockingQueue固定有界单锁数组结构,有界,可指定公平性

2. 核心方法说明

下表列出了 LinkedTransferQueue 的主要方法及其行为特征(基于 JDK 8)。

方法参数返回值阻塞行为异常
LinkedTransferQueue()构造器
LinkedTransferQueue(Collection<? extends E> c)c:初始集合构造器,将集合元素加入队列NullPointerException
put(E e)e:元素void不阻塞(无界),内部调用 offerNullPointerException
offer(E e)e:元素boolean:总是返回 true(无界)不阻塞NullPointerException
offer(E e, long timeout, TimeUnit unit)e:元素,timeout:超时,unit:单位boolean:总是返回 true(超时被忽略)不阻塞NullPointerException
take()E:队首元素如果队列空,阻塞直到有元素InterruptedException
poll()E:队首元素,空返回 null不阻塞
poll(long timeout, TimeUnit unit)timeout:超时,unit:单位E:元素,超时后仍空返回 null等待指定时间InterruptedException
peek()E:队首元素(不移除),空返回 null不阻塞
size()int:当前元素个数无(弱一致性,需遍历链表,非 O(1))
remainingCapacity()int:总是返回 Integer.MAX_VALUE
transfer(E e)e:元素void阻塞直到该元素被消费者 take/poll 取走InterruptedException, NullPointerException
tryTransfer(E e)e:元素boolean:有消费者等待则传递返回 true,否则返回 false(元素不入队)不阻塞NullPointerException
tryTransfer(E e, long timeout, TimeUnit unit)e:元素,timeout:超时,unit:单位boolean:超时前被消费返回 true,否则 false等待指定时间,期间可被中断InterruptedException, NullPointerException
hasWaitingConsumer()boolean:是否有消费者在等待不阻塞
getWaitingConsumerCount()int:等待消费者数量(近似值)不阻塞
drainTo(Collection<? super E> c)c:目标集合int:转移的元素数量NullPointerException
drainTo(Collection<? super E> c, int maxElements)c:目标集合,maxElements:最大转移数int:实际转移数NullPointerException

注意:由于队列无界,offer 系列方法永远不会返回 false(除非元素为 null)。超时版本的 offer 实际上忽略超时参数,行为与无参 offer 相同。

3. 核心原理与源码分析(基于 JDK 8)

3.1 数据结构

LinkedTransferQueue 的核心字段(定义在 java.util.concurrent.LinkedTransferQueue 中):

public class LinkedTransferQueue<E> extends AbstractQueue<E>
    implements TransferQueue<E>, java.io.Serializable {
    
    private transient volatile Node head;   // 队首节点
    private transient volatile Node tail;   // 队尾节点
    private transient volatile int sweepVote; // 用于帮助 GC 的投票计数器
    // ... 其他字段
}

节点 Node 是内部静态类,采用双链表结构:

static final class Node {
    final boolean isData;   // true 表示数据节点(生产者),false 表示请求节点(消费者)
    volatile Object item;   // 数据节点的元素,请求节点为 null
    volatile Node next;     // 后继指针
    volatile Node prev;     // 前驱指针(JDK 8 新增,便于快速移除)
    volatile Thread waiter; // 等待线程(阻塞时记录)
    
    // 构造方法
    Node(Object item, boolean isData) {
        this.item = item;
        this.isData = isData;
    }
    // ...
}
  • isData:区分节点类型。生产者节点(isData=true)携带元素;消费者节点(isData=false)携带 null
  • item:生产者节点存储实际元素,消费者节点为 null。当节点被匹配后,item 会被 CAS 设置为 null(数据节点)或非 null(请求节点),以表示“已取消/已匹配”。
  • prev/next:双链表指针,便于在匹配成功后从链表中快速移除节点(JDK 7 单链表需要从头遍历找到前驱,效率较低)。
  • waiter:阻塞的线程对象,匹配成功后会被唤醒。

双链表的引入大幅提升了移除节点的效率,是 JDK 8 的重要优化。

3.2 xfer 统一核心方法

所有入队、出队、传递操作最终都调用一个私有方法 xfer

private E xfer(E e, boolean haveData, int how, long nanos)
  • 参数 e:元素(生产者传递非 null,消费者传递 null)。
  • 参数 haveDatatrue 表示生产者操作(数据节点),false 表示消费者操作(请求节点)。
  • 参数 how:操作模式,取值如下:
    • NOW = 0:立即返回,不阻塞也不入队(用于 polltryTransfer)。
    • ASYNC = 1:异步入队,不阻塞(用于 putoffer)。
    • SYNC = 2:同步阻塞,直到匹配成功(用于 taketransfer)。
    • TIMED = 3:带超时的阻塞(用于 poll(timeout)tryTransfer(timeout))。
  • 参数 nanos:超时纳秒数,仅在 how=TIMED 时有效。

返回值:对于消费者操作,返回取到的元素;对于生产者操作,通常返回 null(但 transfer 成功时也返回 null)。

主要流程

  1. 尝试匹配等待线程
    head 开始向后遍历,寻找一个模式互补(即 haveData != node.isData)的节点。如果找到,尝试通过 CAS 将节点的 item 设置为匹配值(生产者把 item 从元素改为 null,消费者把 itemnull 改为元素)。

    • CAS 成功:唤醒该节点的等待线程,将匹配节点从链表中断开(unsplice),并返回匹配的元素(消费者操作)或 null(生产者操作)。
    • CAS 失败:说明已被其他线程匹配,继续向后遍历。
  2. 如果没有匹配且允许入队howASYNC/SYNC/TIMED

    • 创建一个新节点(isData=haveDataitem=e),并尝试将其追加到队尾(CAS 更新 tail)。
    • 如果 howASYNC,直接返回 null(生产者的 put/offer 结束)。
    • 如果 howSYNCTIMED,则进入自旋 + 阻塞等待,直到节点被匹配(item 被其他线程改变)或超时/中断。
  3. 阻塞等待细节

    • 首先进行有限次数的自旋(spins),避免在持有锁(虽然是无锁,但自旋可减少上下文切换)的场景下立即阻塞。
    • 自旋未果则调用 LockSupport.park(this) 阻塞当前线程。
    • 被唤醒后检查匹配状态,如果成功则清理节点并返回,否则继续阻塞。

3.3 核心操作的 xfer 调用映射

公开方法调用 xfer 方式说明
put(E e)xfer(e, true, ASYNC, 0)异步入队,不阻塞
offer(E e)xfer(e, true, ASYNC, 0)同上
offer(E e, long timeout, TimeUnit unit)xfer(e, true, ASYNC, 0)(忽略超时)无界队列,超时无意义
take()xfer(null, false, SYNC, 0)阻塞等待元素
poll()xfer(null, false, NOW, 0)非阻塞,立即返回
poll(long timeout, TimeUnit unit)xfer(null, false, TIMED, unit.toNanos(timeout))超时阻塞
transfer(E e)xfer(e, true, SYNC, 0)阻塞直到被消费
tryTransfer(E e)xfer(e, true, NOW, 0)仅尝试匹配,不入队
tryTransfer(E e, long timeout, TimeUnit unit)xfer(e, true, TIMED, unit.toNanos(timeout))超时等待被消费

3.4 公平性保证

LinkedTransferQueue公平 FIFO 的。匹配操作总是从 head 开始向后扫描第一个互补模式的节点。由于入队操作将新节点追加到 tail,等待时间最长的线程位于 head 附近,因此它们会优先被匹配。这避免了不公平的“插队”现象,保证了线程调度的公平性。

3.5 无锁算法核心

整个队列不使用 synchronizedReentrantLock,而是依赖 sun.misc.Unsafe 提供的 CAS 原语。关键操作:

  • casHead / casTail:更新头尾指针。
  • casNext:设置节点的 next 指针。
  • casItem:设置节点的 item 字段,这是匹配的核心——一个节点一旦 item 被成功修改,就意味着匹配完成。

CAS 操作的原子性避免了多线程竞争时的数据不一致,同时不会导致线程阻塞(除了显式的 park)。

3.6 GC 优化:sweep 机制

由于节点被匹配后会从链表中移除,但移除操作本身也需要遍历链表。LinkedTransferQueue 使用一个 sweepVote 计数器来定期触发“清扫”操作,遍历链表并清除那些已经匹配但尚未断开的节点(比如因为并发原因未及时 unsplice)。这有助于减少内存占用,避免“垃圾节点”长期滞留。

3.7 与 SynchronousQueue 对比

  • 容量LinkedTransferQueue 可以缓存多个元素;SynchronousQueue 容量为 0,每个 put 必须配对 take
  • 行为LinkedTransferQueuetransfer(e) 类似于 SynchronousQueueput(e),但前者在没有消费者时会将元素入队并阻塞,后者直接阻塞(不存储)。
  • 适用场景SynchronousQueue 适用于纯 handoff 场景(如 Executors.newCachedThreadPool);LinkedTransferQueue 更灵活,既支持 handoff 也支持缓冲。

4. 必要流程的 Mermaid 图

4.1 类图

classDiagram
    class LinkedTransferQueue~E~ {
        -head : Node
        -tail : Node
        -sweepVote : int
        +put(E e) void
        +offer(E e) boolean
        +take() E
        +poll() E
        +transfer(E e) void
        +tryTransfer(E e) boolean
        +hasWaitingConsumer() boolean
        +getWaitingConsumerCount() int
        -xfer(E e, boolean haveData, int how, long nanos) E
    }
    
    class Node {
        -isData : boolean
        -item : Object
        -next : Node
        -prev : Node
        -waiter : Thread
        +Node(Object item, boolean isData)
    }
    
    LinkedTransferQueue --> Node : head/tail

描述LinkedTransferQueue 持有 headtail 引用指向双链表节点。每个 Node 包含数据/请求标志、元素、前后指针以及等待线程。核心方法 xfer 统一处理各种操作模式。

4.2 链表结构图

graph LR
    head --> Node1
    Node1 --> Node2
    Node2 --> Node3
    Node3 --> tail
    Node1_prev["prev=null"] -.-> Node1
    Node1 -.-> Node2_prev["prev=Node1"]
    Node2 -.-> Node3_prev["prev=Node2"]
    Node3 -.-> tail_prev["prev=Node3"]
    
    subgraph Node1
        N1_mode["mode=DATA (生产者)"]
        N1_item["item=E1"]
    end
    subgraph Node2
        N2_mode["mode=REQUEST (消费者)"]
        N2_item["item=null"]
    end
    subgraph Node3
        N3_mode["mode=DATA"]
        N3_item["item=E2"]
    end

描述: 该图展示了一个包含三个节点的双链表实例。

  • head 指向第一个节点(Node1),它是一个生产者节点(isData=true),携带元素 E1prev 为 null
  • Node2 是消费者节点(isData=false),item 为 null,表示它正在等待一个生产者来匹配。它的 prev 指向 Node1,next 指向 Node3。
  • Node3 是第二个生产者节点,携带元素 E2next 指向 tail
  • tail 是一个哨兵引用,指向最后一个节点(Node3)。注意,tail 并非总是指向真正的尾节点,在并发更新时可能滞后,但最终会通过 CAS 修正。

双链表的优势体现在:当 Node2 被匹配后,可以借助 prev 和 next 直接将其前后节点链接,而无需从头扫描找到前驱,时间复杂度从 O(n) 降为 O(1)。队列的公平性来源于匹配时总是从 head 开始扫描,因此 Node1(等待最久)会优先被匹配。

4.3 xfer 核心流程图(匹配阶段)

graph TD
    Start([xfer 开始]) --> CheckMode{haveData?}
    CheckMode -->|true 生产者| SetMatchNull["期望匹配消费者节点<br>目标item==null"]
    CheckMode -->|false 消费者| SetMatchElem["期望匹配生产者节点<br>目标item!=null"]
    
    SetMatchNull --> ScanHead["从head开始扫描"]
    SetMatchElem --> ScanHead
    
    ScanHead --> FindNode{找到互补模式节点?}
    FindNode -->|否| NoMatch["无匹配,进入入队/返回"]
    FindNode -->|是| CASItem["CAS 设置节点的item字段"]
    
    CASItem --> CASResult{成功?}
    CASResult -->|失败| ScanNext["继续向后扫描"]
    ScanNext --> FindNode
    
    CASResult -->|成功| Wake["唤醒节点上的等待线程"]
    Wake --> Unlink["从链表中移除节点 (unsplice)"]
    Unlink --> Return["返回匹配的值<br>消费者返回元素,生产者返回null"]
    Return --> End([结束])
    
    NoMatch --> End

描述: 该图描述了 xfer 方法中最关键的匹配阶段,即尝试与队列中已存在的等待线程进行“手递手”传递。

  1. 确定目标节点类型

    • 生产者(haveData=true)希望找到一个消费者节点,该节点的 item 当前为 null
    • 消费者(haveData=false)希望找到一个生产者节点,该节点的 item 不为 null(携带具体元素)。
  2. 扫描策略:从 head 开始,沿着 next 指针遍历链表,直到遇到 null(链表末尾)。

    • 之所以从 head 开始,是为了保证 FIFO 公平性:等待时间最长的节点(最靠近 head)优先被匹配,避免线程饥饿。
  3. 匹配尝试:对于每个遍历到的节点,检查其 isData 是否与当前操作互补(即 isData != haveData)。

    • 如果互补,则通过 CAS 尝试修改该节点的 item 字段:

      • 生产者将消费者的 item 从 null 改为非 null(实际元素值)。
      • 消费者将生产者的 item 从非 null 改为 null
    • CAS 成功意味着当前线程赢得了匹配权;如果失败(说明另一个线程已经抢先匹配了该节点),则继续向后扫描。

  4. 匹配成功后的动作

    • 唤醒该节点上阻塞的线程(LockSupport.unpark(waiter))。
    • 调用 unsplice 将匹配节点从双链表中移除,更新 head 或前后节点的指针。
    • 返回相应的值:消费者返回取到的元素,生产者返回 null(表示传递成功)。
  5. 未匹配到任何节点:则进入入队逻辑(how 决定是否入队),或者对于 NOW 模式直接返回。

这个匹配过程是无锁的,多个线程可以同时尝试匹配不同的节点,CAS 保证了原子性。

4.4 入队与阻塞流程

graph TD
    Start([xfer 无匹配]) --> CheckHow{how 的值?}
    CheckHow -->|ASYNC| CreateNode1["创建节点,追加到队尾<br>立即返回"]
    CheckHow -->|NOW| ReturnNow["立即返回 null/false"]
    CheckHow -->|SYNC| CreateNode2["创建节点,追加到队尾"]
    CheckHow -->|TIMED| CreateNode3["创建节点,追加到队尾"]
    
    CreateNode2 --> SelfSpin["有限次自旋<br>检查是否被匹配"]
    CreateNode3 --> SelfSpin
    SelfSpin --> Matched{被匹配?}
    Matched -->|是| Cleanup["清理节点,返回"]
    Matched -->|否| ShouldPark{自旋次数耗尽?}
    ShouldPark -->|否| SelfSpin
    ShouldPark -->|是| Park["LockSupport.park(this)"]
    Park --> Wakeup["被唤醒或中断"]
    Wakeup --> CheckAgain{匹配或超时/中断?}
    CheckAgain -->|匹配| Cleanup
    CheckAgain -->|超时/中断| HandleTimeout["根据情况抛出异常或返回false"]
    
    CreateNode1 --> End([结束])
    ReturnNow --> End
    Cleanup --> End
    HandleTimeout --> End

描述:当匹配阶段未找到互补节点时,根据操作模式 how 决定后续行为。

  • ASYNC 模式putoffer):
    创建一个新节点(isData=haveDataitem=e),通过 CAS 追加到 tail 后面,然后立即返回。因为队列无界,永远不会阻塞。

  • NOW 模式polltryTransfer):
    直接返回 null(消费者)或 false(生产者),不会创建任何节点。这体现了“非阻塞、仅尝试”的语义。

  • SYNC 模式taketransfer)和 TIMED 模式(带超时的 poll 或 tryTransfer):

    1. 创建并入队:与 ASYNC 类似,先创建节点并追加到队尾。此时节点处于“等待匹配”状态。
    2. 自旋等待:为了避免昂贵的线程阻塞/唤醒开销,线程会先进行有限次数的自旋spins,通常基于 CPU 核数动态计算)。自旋期间反复检查节点的 item 是否已被其他线程修改(即是否被匹配)。
    3. 阻塞:如果自旋次数耗尽仍未匹配,则调用 LockSupport.park(this) 将当前线程挂起。TIMED 模式会使用 parkNanos 并设置超时。
    4. 唤醒与检查:当被匹配线程唤醒(或超时/中断)后,再次检查节点状态。如果 item 已被修改(匹配成功),则调用 cleanup 将节点从链表中移除并返回;否则根据超时或中断标志抛出 InterruptedException 或返回 false

自旋机制是性能优化的关键:在锁竞争不激烈或临界区极短的情况下,自旋能避免线程进入内核态,大幅降低延迟。

4.5 transfer 与 take 配对时序图

sequenceDiagram
    participant P as 生产者线程
    participant Q as LinkedTransferQueue
    participant C as 消费者线程
    
    P->>Q: transfer(e)
    activate P
    Q->>Q: xfer(e, true, SYNC)
    Q->>Q: 扫描head,无消费者节点
    Q->>Q: 创建数据节点,追加到tail
    Q->>Q: 自旋后 park()
    Note over P: 生产者阻塞
    
    C->>Q: take()
    activate C
    Q->>Q: xfer(null, false, SYNC)
    Q->>Q: 从head扫描,找到生产者节点
    Q->>Q: CAS 将节点item设为null
    Q->>Q: 唤醒生产者线程
    Q->>C: 返回元素e
    deactivate C
    
    Q->>P: 解除阻塞
    Q->>Q: 清理节点
    P-->>P: transfer返回
    deactivate P

描述:该时序图展示了典型的生产者-消费者通过 transfer 和 take 完成一次“握手”的全过程。

  1. 生产者调用 transfer(e)

    • 内部调用 xfer(e, true, SYNC, 0)
    • 匹配阶段扫描链表,发现没有等待的消费者节点(队列可能为空或全是生产者节点)。
    • 创建一个新的数据节点(isData=trueitem=e),通过 CAS 追加到 tail
    • 由于 how=SYNC,节点入队后,生产者线程进入自旋,随后调用 park() 阻塞。
  2. 消费者调用 take()

    • 内部调用 xfer(null, false, SYNC, 0)
    • 匹配阶段从 head 开始扫描,找到了生产者节点(isData=true 且 item=e),两者互补。
    • 消费者通过 CAS 将该节点的 item 从 e 修改为 null。CAS 成功,表示消费者赢得了匹配权。
    • 消费者唤醒生产者节点中保存的 waiter 线程(即生产者线程)。
    • 消费者返回被取出的元素 etake() 结束。
  3. 生产者被唤醒

    • 从 park() 返回,检查到节点的 item 已被改为 null(匹配成功)。
    • 调用清理逻辑将节点从链表中移除。
    • transfer(e) 返回 void,生产者继续执行。

注意:如果消费者在生产者之前调用 take(),则角色互换——消费者节点会先入队并阻塞,直到生产者到来并匹配它。整个机制是对称的。

4.6 tryTransfer 的快速路径

graph LR
    A([tryTransfer e]) --> B[xfer e, true, NOW, 0]
    B --> C[从 head 扫描消费者节点]
    C --> D{找到等待消费者?}
    D -->|是| E[CAS 匹配节点<br>唤醒消费者]
    E --> F[返回 true]
    D -->|否| G[返回 false<br>元素不入队]
    F --> H([结束])
    G --> H

描述tryTransfer 是 transfer 的非阻塞版本,其行为由 how=NOW 决定。

  • 不会创建节点:即使用户调用 tryTransfer(e) 时没有等待的消费者,元素 e 也不会被放入队列。这与其他 offer 方法完全不同。

  • 仅尝试匹配已有消费者:从 head 开始扫描,寻找一个消费者节点(isData=false 且 item==null)。

    • 如果找到,则通过 CAS 将该消费者的 item 从 null 设置为 e,唤醒消费者线程,并返回 true
    • 如果未找到,立即返回 false,元素 e 被丢弃(应用层需要自行处理,例如重试、持久化)。
  • 不阻塞、不超时:整个操作是纯 CPU 操作,不会导致线程挂起。

此方法适用于“如果消费者恰好正在等待,就传递;否则放弃”的场景,例如在实时系统中避免生产者因等待而阻塞。

4.7 多线程并发下的匹配与出队示意图

graph TD
    subgraph 初始队列
        H0[head] --> N1[消费者节点A<br>等待]
        N1 --> N2[生产者节点B<br>等待]
        N2 --> T0[tail]
    end
    
    subgraph 新生产者C到达
        C[新生产者C<br>调用put]
    end
    
    C --> Match{匹配?}
    Match -->|模式互补| N1[与消费者节点A匹配]
    N1 --> RemoveA[移除节点A]
    RemoveA --> NewHead[head指向原N2]
    
    NewHead --> FinalQueue[最终队列<br>head->生产者节点B<br>tail不变]

描述:该图演示了多线程并发环境下,一个新生产者到来时如何与队列头部等待的消费者匹配,并更新链表结构。

  • 初始状态:队列中有两个节点。

    • 节点 A:消费者(isData=false),处于等待状态。
    • 节点 B:生产者(isData=true),也处于等待状态(例如之前调用 transfer 但未匹配)。
      head 指向 A,tail 指向 B。
  • 新生产者 C 调用 put(e)

    • put 对应 xfer(e, true, ASYNC, 0),首先执行匹配阶段。
    • 从 head 开始扫描,第一个节点 A 是消费者,与生产者 C 互补。
    • 生产者 C 尝试 CAS 修改节点 A 的 item(从 null 改为 e)。CAS 成功。
    • 唤醒节点 A 上阻塞的消费者线程,并将节点 A 从链表中移除(unsplice)。
  • 移除节点 A 的过程

    • 节点 A 的 prev 为 null(因为它是 head),next 指向节点 B。
    • 将 head 通过 CAS 从 A 更新为 B(节点 B 成为新的 head)。
    • 节点 B 的 prev 被设置为 null,断开了与 A 的链接。
    • 节点 A 不再被任何活跃引用,可被 GC 回收。
  • 最终队列head 指向节点 B(生产者),tail 仍指向 B(如果 B 是最后一个节点)。

这个例子展示了 并发匹配和出队 的典型流程:新来的生产者没有创建新节点,而是直接与已存在的消费者握手,并将消费者节点从队列中移除,实现了“手递手”且队列长度减少的效果。整个过程无锁,依赖 CAS 保证原子性。


5. 实际应用场景与代码举例(JDK 8 兼容)

以下所有示例均可在 JDK 8 环境下编译运行。假设类名为 LinkedTransferQueueDemo,请自行包含在同一个文件中。

5.1 生产者等待消费者确认(transfer)

场景:消息发送者必须确保消息被接收后才继续发送下一条,保证可靠交付。

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

public class TransferConfirmDemo {
    public static void main(String[] args) throws InterruptedException {
        TransferQueue<String> queue = new LinkedTransferQueue<>();
        
        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                String msg = queue.take();
                System.out.println("消费者收到: " + msg);
                // 模拟处理耗时
                Thread.sleep(500);
                System.out.println("消费者处理完成");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        consumer.start();
        
        // 确保消费者先启动,或者不保证也可演示
        Thread.sleep(100);
        
        // 生产者使用 transfer,等待消费者取走
        System.out.println("生产者开始 transfer...");
        queue.transfer("重要消息");
        System.out.println("生产者确认消息已被消费,继续执行");
    }
}

输出(消费者稍后处理完成):

生产者开始 transfer...
消费者收到: 重要消息
消费者处理完成
生产者确认消息已被消费,继续执行

5.2 尝试立即传递(tryTransfer)

场景:如果消费者未就绪,生产者立即放弃并执行备选逻辑(如记录日志、暂存数据库)。

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

public class TryTransferDemo {
    public static void main(String[] args) {
        TransferQueue<String> queue = new LinkedTransferQueue<>();
        
        // 没有消费者启动
        boolean success = queue.tryTransfer("即时消息");
        if (success) {
            System.out.println("消息已被消费者接收");
        } else {
            System.out.println("无消费者等待,消息未送达,转存至数据库");
        }
        
        // 启动一个消费者
        new Thread(() -> {
            try {
                String msg = queue.take();
                System.out.println("消费者收到: " + msg);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
        
        // 等待消费者进入等待状态
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        
        // 再次尝试
        success = queue.tryTransfer("第二条消息");
        System.out.println("第二次尝试结果: " + success);
    }
}

输出

无消费者等待,消息未送达,转存至数据库
消费者收到: 第二条消息
第二次尝试结果: true

5.3 带超时的 tryTransfer

场景:生产者等待消费者一段时间,若无人接收则回退。

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TransferQueue;

public class TryTransferTimeoutDemo {
    public static void main(String[] args) throws InterruptedException {
        TransferQueue<String> queue = new LinkedTransferQueue<>();
        
        // 生产者尝试等待 1 秒
        boolean success = queue.tryTransfer("超时消息", 1, TimeUnit.SECONDS);
        if (!success) {
            System.out.println("1秒内无消费者,消息进入备用队列");
        }
        
        // 启动一个延迟消费者
        new Thread(() -> {
            try {
                Thread.sleep(1500); // 晚于超时时间
                String msg = queue.take();
                System.out.println("消费者收到: " + msg);
            } catch (InterruptedException e) {}
        }).start();
        
        // 再次尝试,等待 3 秒
        success = queue.tryTransfer("延迟消息", 3, TimeUnit.SECONDS);
        System.out.println("第二次尝试结果: " + success);
    }
}

输出

1秒内无消费者,消息进入备用队列
第二次尝试结果: true
消费者收到: 延迟消息

5.4 生产者-消费者缓冲模式(对比 LinkedBlockingQueue)

场景:大量数据生产消费,对比 LinkedTransferQueueLinkedBlockingQueue 的吞吐量(本示例仅展示使用方式,实际性能测试需要更严谨的基准)。

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;

public class ThroughputCompare {
    private static final int PRODUCERS = 2;
    private static final int CONSUMERS = 2;
    private static final int TASKS_PER_PRODUCER = 100_000;
    
    public static void main(String[] args) throws InterruptedException {
        // 测试 LinkedTransferQueue
        BlockingQueue<Integer> transferQueue = new LinkedTransferQueue<>();
        long time1 = testQueue(transferQueue);
        System.out.println("LinkedTransferQueue 耗时: " + time1 + " ms");
        
        // 测试 LinkedBlockingQueue
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
        long time2 = testQueue(blockingQueue);
        System.out.println("LinkedBlockingQueue 耗时: " + time2 + " ms");
    }
    
    private static long testQueue(BlockingQueue<Integer> queue) throws InterruptedException {
        AtomicLong total = new AtomicLong();
        Thread[] producers = new Thread[PRODUCERS];
        Thread[] consumers = new Thread[CONSUMERS];
        
        // 消费者
        for (int i = 0; i < CONSUMERS; i++) {
            consumers[i] = new Thread(() -> {
                try {
                    while (true) {
                        Integer v = queue.take();
                        if (v == -1) break; // 终止信号
                        total.incrementAndGet();
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            consumers[i].start();
        }
        
        long start = System.currentTimeMillis();
        // 生产者
        for (int i = 0; i < PRODUCERS; i++) {
            final int id = i;
            producers[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < TASKS_PER_PRODUCER; j++) {
                        queue.put(j);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            producers[i].start();
        }
        
        // 等待生产者完成
        for (Thread p : producers) p.join();
        // 发送终止信号
        for (int i = 0; i < CONSUMERS; i++) queue.put(-1);
        for (Thread c : consumers) c.join();
        
        long end = System.currentTimeMillis();
        System.out.println("处理数量: " + total.get());
        return end - start;
    }
}

注意:实际运行中 LinkedTransferQueue 通常表现更优,但结果受 JVM、CPU 核数等影响。

5.5 获取等待消费者信息

场景:监控系统动态调整生产速率,避免过度生产。

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

public class MonitorDemo {
    public static void main(String[] args) throws InterruptedException {
        TransferQueue<String> queue = new LinkedTransferQueue<>();
        
        // 启动消费者,但消费慢
        Thread slowConsumer = new Thread(() -> {
            try {
                while (true) {
                    String item = queue.take();
                    System.out.println("消费: " + item);
                    Thread.sleep(500); // 模拟慢消费
                }
            } catch (InterruptedException e) {}
        });
        slowConsumer.setDaemon(true);
        slowConsumer.start();
        
        // 监控线程
        Thread monitor = new Thread(() -> {
            while (true) {
                System.out.printf("等待消费者数: %d, 队列大小: %d%n",
                        queue.getWaitingConsumerCount(), queue.size());
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
            }
        });
        monitor.setDaemon(true);
        monitor.start();
        
        // 生产者生产 10 个元素
        for (int i = 1; i <= 10; i++) {
            queue.put("消息" + i);
            System.out.println("生产: 消息" + i);
            Thread.sleep(100);
        }
        
        Thread.sleep(3000); // 让监控输出一会
    }
}

输出片段

等待消费者数: 1, 队列大小: 0
生产: 消息1
消费: 消息1
生产: 消息2
等待消费者数: 1, 队列大小: 0
消费: 消息2
...

6. 吞吐量与性能分析

6.1 无锁算法的性能优势

  • 无上下文切换:传统锁(如 LinkedBlockingQueueReentrantLock)在竞争激烈时会导致线程阻塞和唤醒,引发大量上下文切换。LinkedTransferQueue 基于 CAS,大多数操作在用户态自旋完成,失败时也仅短暂阻塞(通过 LockSupport.park),整体上减少了内核态切换。
  • 更高的并发度:多线程可以同时尝试入队、出队,不同节点上的 CAS 操作互不干扰(例如,tail 的更新和 head 的更新可以并发进行),而双锁队列的 takeLockputLock 虽然分离,但仍有竞争。

6.2 节点匹配的扫描开销

  • 最坏情况 O(n):当队列中有大量等待节点时,每次 xfer 都需要从 head 开始扫描,直到找到互补节点或到达 tail。极端情况下(如所有节点都是同一模式),扫描会遍历整个队列,导致性能下降。
  • 实际表现:通常队列长度不会太大(因为匹配会及时移除节点),且扫描是内存局部性较好的链表遍历,开销可控。此外,JDK 8 中的双链表和启发式跳过(如 sweep)优化了扫描效率。

6.3 内存占用

  • 每个元素包装为一个 Node 对象,包含 prevnextitemwaiterisData 等字段。相比 LinkedBlockingQueueNode(只有 nextitem),内存开销更大(多了 prevwaiter)。
  • 相比 SynchronousQueue 的节点(TransferStackTransferQueue 节点),两者复杂度相近,但 SynchronousQueue 的节点数量与并发线程数相关,而 LinkedTransferQueue 的节点数量与未匹配元素数相关。

6.4 与 LinkedBlockingQueue 对比

维度LinkedTransferQueueLinkedBlockingQueue
锁机制无锁(CAS)双锁(takeLock/putLock)
吞吐量(高并发)通常更高锁竞争可能成为瓶颈
size() 操作O(n),弱一致O(1),维护一个 AtomicInteger
transfer 支持
内存占用(每元素)较大(双链表 + waiter)较小(单链表)

6.5 与 SynchronousQueue 对比

  • 纯 handoff 场景SynchronousQueue 可能更快,因为它不维护链表结构(采用栈或队列,但节点数不超过并发线程数),匹配操作更直接。
  • 混合场景LinkedTransferQueue 更灵活,既能缓冲又能直接传递,且公平模式下 SynchronousQueue 的队列实现本质上也是链表,性能差异不大。

6.6 性能调优建议

  1. 避免频繁调用 size():该方法需要遍历整个链表,时间复杂度 O(n),且返回值是弱一致的(不代表精确大小)。可使用 isEmpty() 判断空。
  2. 谨慎使用 transfer:如果消费者速度跟不上,生产者会永久阻塞,容易导致线程池耗尽。建议配合超时或使用 tryTransfer
  3. 对于纯缓冲场景:使用 put/take 即可,无需启用 transfer 语义。
  4. 合理设置并发度:虽然是无锁队列,但过多的线程同时竞争同一个 head/tail 的 CAS 仍然会导致自旋重试,适当限制并发数可提升性能。

7. 注意事项与常见陷阱

注意事项原因和解决方案
size() 是 O(n) 操作需要遍历整个链表才能计数,高并发下频繁调用会严重影响性能。使用 isEmpty() 替代判空。
remainingCapacity() 永远返回 Integer.MAX_VALUE因为队列无界,不要依赖此方法做容量控制。
transfer 可能永久阻塞如果没有消费者调用 take/poll,生产者会一直阻塞。应使用 tryTransfer 或带超时的版本,或设计消费保证。
tryTransfer 不保证元素入队返回 false 时元素未被任何线程接收,需要应用层自行处理(如重试、持久化)。
pollpeek 可能返回 null正确判空,避免 NullPointerException
公平性带来的性能权衡FIFO 公平匹配比 LIFO(如某些栈实现)略慢,但避免了线程饥饿。
无锁算法导致的调试复杂性内部实现非常复杂,普通开发者不应修改;使用时只需关注 API。
元素不可为 null与大多数 BlockingQueue 实现一致,插入 null 会抛出 NullPointerException
内存可见性由 CAS 保证不需要额外加 volatile 或同步,LinkedTransferQueue 已确保线程安全。

8. 与其他阻塞队列的对比总结

队列有界性数据结构锁机制支持 transfersize 复杂度公平性典型应用场景
LinkedTransferQueue无界双链表无锁(CAS)O(n)公平 FIFO高吞吐、需要确认送达、缓冲 + handoff 混合
LinkedBlockingQueue可选有界(默认无界)单链表双锁(take/put 分离)O(1)非公平(默认)传统生产者-消费者缓冲,容量可限制
SynchronousQueue0(零容量)栈/队列无锁(CAS)是(本质 handoff)O(1)可配置(公平/非公平)线程池 handoff(如 CachedThreadPool),无缓冲交换
ArrayBlockingQueue固定有界数组单锁(ReentrantLockO(1)可配置(公平/非公平)固定大小缓冲区,资源受限场景

补充说明

  • LinkedTransferQueuesize() 复杂度为 O(n),而其他三种队列都可以在 O(1) 时间内返回大小(SynchronousQueue 总是返回 0)。
  • SynchronousQueue 的公平模式使用队列(FIFO),非公平模式使用栈(LIFO);LinkedTransferQueue 固定为 FIFO。
  • ArrayBlockingQueue 使用单锁,在生产和消费同时进行时有一定竞争,但数组结构缓存友好。

9. 总结与学习指引

核心特点回顾

LinkedTransferQueue 是 Java 并发工具包中设计精妙的无锁阻塞队列,其独特之处在于:

  • 无界双链表:动态扩展,永不阻塞生产者(除 transfer 外)。
  • 统一 xfer 模型:所有操作都通过一个核心方法实现,代码复用度高。
  • 无锁 + CAS:避免锁竞争,提高并发吞吐量。
  • transfer 语义:填补了普通队列和 SynchronousQueue 之间的空白,提供生产者驱动的确认机制。
  • 公平 FIFO:保证先到先服务,防止饥饿。

使用建议

  • 需要生产者确认消费:使用 transfer 或带超时的 tryTransfer
  • 高吞吐量缓冲:使用 put / take,比 LinkedBlockingQueue 通常有更好的伸缩性。
  • 避免依赖 size():其 O(n) 开销和弱一致性可能误导业务逻辑。
  • 处理 tryTransfer 失败:实现回退策略,避免数据丢失。
  • SynchronousQueue 取舍:纯 handoff 且不需要缓存时,SynchronousQueue 可能更轻量;需要缓冲或混合模式时,选 LinkedTransferQueue