1. 概述
LinkedTransferQueue 是 Java 并发包中一个强大且复杂的阻塞队列实现。它实现了 TransferQueue 接口,融合了 SynchronousQueue 的直接传递 (handoff)特性和 LinkedBlockingQueue 的缓冲能力。简单来说,它既允许生产者将元素放入队列供消费者后续获取,也支持生产者阻塞等待消费者直接取走元素,而不经过队列的"中间缓存"。
核心特点
- 无界队列 :基于链表实现,理论上容量受限于内存,永远不会因容量满而阻塞生产者(
put永远不会阻塞)。 - 支持 transfer 语义 :生产者可以通过
transfer(e)阻塞等待,直到某个消费者取走该元素;tryTransfer(e)可尝试立即传递,失败则立即返回而不入队。 - 无锁算法 :内部大量使用
CAS(Compare-And-Swap)操作,避免使用显式锁(如ReentrantLock),在高并发场景下提供更好的性能。 - 公平 FIFO 匹配:匹配等待线程时,总是从队列头部(head)开始扫描,保证等待时间最长的线程先被服务,避免饥饿。
- 双链表结构 :JDK 8 中
Node包含prev和next指针,便于快速移除已匹配的节点,相比 JDK 7 的单链表有较大改进。
典型应用场景
- 生产者需要确认消费者已接收:例如消息中间件中,发送端要求确保消息被接收端处理后才继续后续操作。
- 高吞吐量的数据交换:无锁设计减少了线程上下文切换,适合多生产者多消费者环境。
- 实现轻量级的 Exchanger :利用
transfer的配对行为,两个线程可以安全地交换数据。
与其他队列的主要区别
| 队列 | 容量 | 锁机制 | 是否支持 transfer | 特点 |
|---|---|---|---|---|
| LinkedTransferQueue | 无界 | 无锁(CAS) | 是 | 可缓冲可 handoff,公平 FIFO,高吞吐 |
| LinkedBlockingQueue | 可选有界(默认无界) | 双锁(takeLock/putLock) | 否 | 有锁,size O(1),适合传统生产者-消费者缓冲 |
| SynchronousQueue | 0(零容量) | 无锁(CAS) | 是(本质就是 handoff) | 每一个 put 必须等待一个 take,无缓冲,常用于线程池 |
| ArrayBlockingQueue | 固定有界 | 单锁 | 否 | 数组结构,有界,可指定公平性 |
2. 核心方法说明
下表列出了 LinkedTransferQueue 的主要方法及其行为特征(基于 JDK 8)。
| 方法 | 参数 | 返回值 | 阻塞行为 | 异常 |
|---|---|---|---|---|
LinkedTransferQueue() |
无 | 构造器 | 无 | 无 |
LinkedTransferQueue(Collection<? extends E> c) |
c:初始集合 |
构造器,将集合元素加入队列 | 无 | NullPointerException |
put(E e) |
e:元素 |
void |
不阻塞(无界),内部调用 offer |
NullPointerException |
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 中):
java
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 是内部静态类,采用双链表结构:
java
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:
java
private E xfer(E e, boolean haveData, int how, long nanos)
- 参数
e:元素(生产者传递非null,消费者传递null)。 - 参数
haveData:true表示生产者操作(数据节点),false表示消费者操作(请求节点)。 - 参数
how:操作模式,取值如下:NOW= 0:立即返回,不阻塞也不入队(用于poll、tryTransfer)。ASYNC= 1:异步入队,不阻塞(用于put、offer)。SYNC= 2:同步阻塞,直到匹配成功(用于take、transfer)。TIMED= 3:带超时的阻塞(用于poll(timeout)、tryTransfer(timeout))。
- 参数
nanos:超时纳秒数,仅在how=TIMED时有效。
返回值 :对于消费者操作,返回取到的元素;对于生产者操作,通常返回 null(但 transfer 成功时也返回 null)。
主要流程
-
尝试匹配等待线程
从
head开始向后遍历,寻找一个模式互补 (即haveData != node.isData)的节点。如果找到,尝试通过 CAS 将节点的item设置为匹配值(生产者把item从元素改为null,消费者把item从null改为元素)。- CAS 成功:唤醒该节点的等待线程,将匹配节点从链表中断开(
unsplice),并返回匹配的元素(消费者操作)或null(生产者操作)。 - CAS 失败:说明已被其他线程匹配,继续向后遍历。
- CAS 成功:唤醒该节点的等待线程,将匹配节点从链表中断开(
-
如果没有匹配且允许入队 (
how为ASYNC/SYNC/TIMED)- 创建一个新节点(
isData=haveData,item=e),并尝试将其追加到队尾(CAS 更新tail)。 - 如果
how是ASYNC,直接返回null(生产者的put/offer结束)。 - 如果
how是SYNC或TIMED,则进入自旋 + 阻塞等待,直到节点被匹配(item被其他线程改变)或超时/中断。
- 创建一个新节点(
-
阻塞等待细节
- 首先进行有限次数的自旋(
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 无锁算法核心
整个队列不使用 synchronized 或 ReentrantLock,而是依赖 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。 - 行为 :
LinkedTransferQueue的transfer(e)类似于SynchronousQueue的put(e),但前者在没有消费者时会将元素入队并阻塞,后者直接阻塞(不存储)。 - 适用场景 :
SynchronousQueue适用于纯 handoff 场景(如Executors.newCachedThreadPool);LinkedTransferQueue更灵活,既支持 handoff 也支持缓冲。
4. 必要流程的 Mermaid 图
4.1 类图
描述 :LinkedTransferQueue 持有 head 和 tail 引用指向双链表节点。每个 Node 包含数据/请求标志、元素、前后指针以及等待线程。核心方法 xfer 统一处理各种操作模式。
4.2 链表结构图
描述: 该图展示了一个包含三个节点的双链表实例。
head指向第一个节点(Node1),它是一个生产者节点(isData=true),携带元素E1,prev为null。- Node2 是消费者节点(
isData=false),item为null,表示它正在等待一个生产者来匹配。它的prev指向 Node1,next指向 Node3。 - Node3 是第二个生产者节点,携带元素
E2,next指向tail。 tail是一个哨兵引用,指向最后一个节点(Node3)。注意,tail并非总是指向真正的尾节点,在并发更新时可能滞后,但最终会通过 CAS 修正。
双链表的优势体现在:当 Node2 被匹配后,可以借助 prev 和 next 直接将其前后节点链接,而无需从头扫描找到前驱,时间复杂度从 O(n) 降为 O(1)。队列的公平性来源于匹配时总是从 head 开始扫描,因此 Node1(等待最久)会优先被匹配。
4.3 xfer 核心流程图(匹配阶段)
目标item==null"] CheckMode -->|false 消费者| SetMatchElem["期望匹配生产者节点
目标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["返回匹配的值
消费者返回元素,生产者返回null"] Return --> End([结束]) NoMatch --> End
描述 : 该图描述了 xfer 方法中最关键的匹配阶段,即尝试与队列中已存在的等待线程进行"手递手"传递。
-
确定目标节点类型:
- 生产者(
haveData=true)希望找到一个消费者节点,该节点的item当前为null。 - 消费者(
haveData=false)希望找到一个生产者节点,该节点的item不为null(携带具体元素)。
- 生产者(
-
扫描策略 :从
head开始,沿着next指针遍历链表,直到遇到null(链表末尾)。- 之所以从
head开始,是为了保证 FIFO 公平性 :等待时间最长的节点(最靠近head)优先被匹配,避免线程饥饿。
- 之所以从
-
匹配尝试 :对于每个遍历到的节点,检查其
isData是否与当前操作互补(即isData != haveData)。-
如果互补,则通过
CAS尝试修改该节点的item字段:- 生产者将消费者的
item从null改为非null(实际元素值)。 - 消费者将生产者的
item从非null改为null。
- 生产者将消费者的
-
CAS 成功意味着当前线程赢得了匹配权;如果失败(说明另一个线程已经抢先匹配了该节点),则继续向后扫描。
-
-
匹配成功后的动作:
- 唤醒该节点上阻塞的线程(
LockSupport.unpark(waiter))。 - 调用
unsplice将匹配节点从双链表中移除,更新head或前后节点的指针。 - 返回相应的值:消费者返回取到的元素,生产者返回
null(表示传递成功)。
- 唤醒该节点上阻塞的线程(
-
未匹配到任何节点 :则进入入队逻辑(
how决定是否入队),或者对于NOW模式直接返回。
这个匹配过程是无锁的,多个线程可以同时尝试匹配不同的节点,CAS 保证了原子性。
4.4 入队与阻塞流程
立即返回"] CheckHow -->|NOW| ReturnNow["立即返回 null/false"] CheckHow -->|SYNC| CreateNode2["创建节点,追加到队尾"] CheckHow -->|TIMED| CreateNode3["创建节点,追加到队尾"] CreateNode2 --> SelfSpin["有限次自旋
检查是否被匹配"] 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模式 (put、offer):创建一个新节点(
isData=haveData,item=e),通过 CAS 追加到tail后面,然后立即返回。因为队列无界,永远不会阻塞。 -
NOW模式 (poll、tryTransfer):直接返回
null(消费者)或false(生产者),不会创建任何节点。这体现了"非阻塞、仅尝试"的语义。 -
SYNC模式 (take、transfer)和TIMED模式 (带超时的poll或tryTransfer):- 创建并入队 :与
ASYNC类似,先创建节点并追加到队尾。此时节点处于"等待匹配"状态。 - 自旋等待 :为了避免昂贵的线程阻塞/唤醒开销,线程会先进行有限次数的自旋 (
spins,通常基于 CPU 核数动态计算)。自旋期间反复检查节点的item是否已被其他线程修改(即是否被匹配)。 - 阻塞 :如果自旋次数耗尽仍未匹配,则调用
LockSupport.park(this)将当前线程挂起。TIMED模式会使用parkNanos并设置超时。 - 唤醒与检查 :当被匹配线程唤醒(或超时/中断)后,再次检查节点状态。如果
item已被修改(匹配成功),则调用cleanup将节点从链表中移除并返回;否则根据超时或中断标志抛出InterruptedException或返回false。
- 创建并入队 :与
自旋机制是性能优化的关键:在锁竞争不激烈或临界区极短的情况下,自旋能避免线程进入内核态,大幅降低延迟。
4.5 transfer 与 take 配对时序图
描述 :该时序图展示了典型的生产者-消费者通过 transfer 和 take 完成一次"握手"的全过程。
-
生产者调用
transfer(e):- 内部调用
xfer(e, true, SYNC, 0)。 - 匹配阶段扫描链表,发现没有等待的消费者节点(队列可能为空或全是生产者节点)。
- 创建一个新的数据节点(
isData=true,item=e),通过 CAS 追加到tail。 - 由于
how=SYNC,节点入队后,生产者线程进入自旋,随后调用park()阻塞。
- 内部调用
-
消费者调用
take():- 内部调用
xfer(null, false, SYNC, 0)。 - 匹配阶段从
head开始扫描,找到了生产者节点(isData=true且item=e),两者互补。 - 消费者通过 CAS 将该节点的
item从e修改为null。CAS 成功,表示消费者赢得了匹配权。 - 消费者唤醒生产者节点中保存的
waiter线程(即生产者线程)。 - 消费者返回被取出的元素
e,take()结束。
- 内部调用
-
生产者被唤醒:
- 从
park()返回,检查到节点的item已被改为null(匹配成功)。 - 调用清理逻辑将节点从链表中移除。
transfer(e)返回void,生产者继续执行。
- 从
注意:如果消费者在生产者之前调用 take(),则角色互换------消费者节点会先入队并阻塞,直到生产者到来并匹配它。整个机制是对称的。
4.6 tryTransfer 的快速路径
唤醒消费者] E --> F[返回 true] D -->|否| G[返回 false
元素不入队] F --> H([结束]) G --> H
描述 :tryTransfer 是 transfer 的非阻塞版本,其行为由 how=NOW 决定。
-
不会创建节点 :即使用户调用
tryTransfer(e)时没有等待的消费者,元素e也不会被放入队列 。这与其他offer方法完全不同。 -
仅尝试匹配已有消费者 :从
head开始扫描,寻找一个消费者节点(isData=false且item==null)。- 如果找到,则通过 CAS 将该消费者的
item从null设置为e,唤醒消费者线程,并返回true。 - 如果未找到,立即返回
false,元素e被丢弃(应用层需要自行处理,例如重试、持久化)。
- 如果找到,则通过 CAS 将该消费者的
-
不阻塞、不超时:整个操作是纯 CPU 操作,不会导致线程挂起。
此方法适用于"如果消费者恰好正在等待,就传递;否则放弃"的场景,例如在实时系统中避免生产者因等待而阻塞。
4.7 多线程并发下的匹配与出队示意图
等待] N1 --> N2[生产者节点B
等待] N2 --> T0[tail] end subgraph 新生产者C到达 C[新生产者C
调用put] end C --> Match{匹配?} Match -->|模式互补| N1[与消费者节点A匹配] N1 --> RemoveA[移除节点A] RemoveA --> NewHead[head指向原N2] NewHead --> FinalQueue[最终队列
head->生产者节点B
tail不变]
描述:该图演示了多线程并发环境下,一个新生产者到来时如何与队列头部等待的消费者匹配,并更新链表结构。
-
初始状态:队列中有两个节点。
- 节点 A:消费者(
isData=false),处于等待状态。 - 节点 B:生产者(
isData=true),也处于等待状态(例如之前调用transfer但未匹配)。
head指向 A,tail指向 B。
- 节点 A:消费者(
-
新生产者 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 回收。
- 节点 A 的
-
最终队列 :
head指向节点 B(生产者),tail仍指向 B(如果 B 是最后一个节点)。
这个例子展示了 并发匹配和出队 的典型流程:新来的生产者没有创建新节点,而是直接与已存在的消费者握手,并将消费者节点从队列中移除,实现了"手递手"且队列长度减少的效果。整个过程无锁,依赖 CAS 保证原子性。
5. 实际应用场景与代码举例(JDK 8 兼容)
以下所有示例均可在 JDK 8 环境下编译运行。假设类名为 LinkedTransferQueueDemo,请自行包含在同一个文件中。
5.1 生产者等待消费者确认(transfer)
场景:消息发送者必须确保消息被接收后才继续发送下一条,保证可靠交付。
java
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("生产者确认消息已被消费,继续执行");
}
}
输出(消费者稍后处理完成):
arduino
生产者开始 transfer...
消费者收到: 重要消息
消费者处理完成
生产者确认消息已被消费,继续执行
5.2 尝试立即传递(tryTransfer)
场景:如果消费者未就绪,生产者立即放弃并执行备选逻辑(如记录日志、暂存数据库)。
java
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);
}
}
输出:
makefile
无消费者等待,消息未送达,转存至数据库
消费者收到: 第二条消息
第二次尝试结果: true
5.3 带超时的 tryTransfer
场景:生产者等待消费者一段时间,若无人接收则回退。
java
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);
}
}
输出:
makefile
1秒内无消费者,消息进入备用队列
第二次尝试结果: true
消费者收到: 延迟消息
5.4 生产者-消费者缓冲模式(对比 LinkedBlockingQueue)
场景 :大量数据生产消费,对比 LinkedTransferQueue 和 LinkedBlockingQueue 的吞吐量(本示例仅展示使用方式,实际性能测试需要更严谨的基准)。
java
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 获取等待消费者信息
场景:监控系统动态调整生产速率,避免过度生产。
java
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); // 让监控输出一会
}
}
输出片段:
makefile
等待消费者数: 1, 队列大小: 0
生产: 消息1
消费: 消息1
生产: 消息2
等待消费者数: 1, 队列大小: 0
消费: 消息2
...
6. 吞吐量与性能分析
6.1 无锁算法的性能优势
- 无上下文切换 :传统锁(如
LinkedBlockingQueue的ReentrantLock)在竞争激烈时会导致线程阻塞和唤醒,引发大量上下文切换。LinkedTransferQueue基于 CAS,大多数操作在用户态自旋完成,失败时也仅短暂阻塞(通过LockSupport.park),整体上减少了内核态切换。 - 更高的并发度 :多线程可以同时尝试入队、出队,不同节点上的 CAS 操作互不干扰(例如,
tail的更新和head的更新可以并发进行),而双锁队列的takeLock和putLock虽然分离,但仍有竞争。
6.2 节点匹配的扫描开销
- 最坏情况 O(n) :当队列中有大量等待节点时,每次
xfer都需要从head开始扫描,直到找到互补节点或到达tail。极端情况下(如所有节点都是同一模式),扫描会遍历整个队列,导致性能下降。 - 实际表现 :通常队列长度不会太大(因为匹配会及时移除节点),且扫描是内存局部性较好的链表遍历,开销可控。此外,JDK 8 中的双链表和启发式跳过(如
sweep)优化了扫描效率。
6.3 内存占用
- 每个元素包装为一个
Node对象,包含prev、next、item、waiter、isData等字段。相比LinkedBlockingQueue的Node(只有next和item),内存开销更大(多了prev和waiter)。 - 相比
SynchronousQueue的节点(TransferStack或TransferQueue节点),两者复杂度相近,但SynchronousQueue的节点数量与并发线程数相关,而LinkedTransferQueue的节点数量与未匹配元素数相关。
6.4 与 LinkedBlockingQueue 对比
| 维度 | LinkedTransferQueue | LinkedBlockingQueue |
|---|---|---|
| 锁机制 | 无锁(CAS) | 双锁(takeLock/putLock) |
| 吞吐量(高并发) | 通常更高 | 锁竞争可能成为瓶颈 |
| size() 操作 | O(n),弱一致 | O(1),维护一个 AtomicInteger |
| transfer 支持 | 是 | 否 |
| 内存占用(每元素) | 较大(双链表 + waiter) | 较小(单链表) |
6.5 与 SynchronousQueue 对比
- 纯 handoff 场景 :
SynchronousQueue可能更快,因为它不维护链表结构(采用栈或队列,但节点数不超过并发线程数),匹配操作更直接。 - 混合场景 :
LinkedTransferQueue更灵活,既能缓冲又能直接传递,且公平模式下SynchronousQueue的队列实现本质上也是链表,性能差异不大。
6.6 性能调优建议
- 避免频繁调用
size():该方法需要遍历整个链表,时间复杂度 O(n),且返回值是弱一致的(不代表精确大小)。可使用isEmpty()判断空。 - 谨慎使用
transfer:如果消费者速度跟不上,生产者会永久阻塞,容易导致线程池耗尽。建议配合超时或使用tryTransfer。 - 对于纯缓冲场景 :使用
put/take即可,无需启用 transfer 语义。 - 合理设置并发度 :虽然是无锁队列,但过多的线程同时竞争同一个
head/tail的 CAS 仍然会导致自旋重试,适当限制并发数可提升性能。
7. 注意事项与常见陷阱
| 注意事项 | 原因和解决方案 |
|---|---|
size() 是 O(n) 操作 |
需要遍历整个链表才能计数,高并发下频繁调用会严重影响性能。使用 isEmpty() 替代判空。 |
remainingCapacity() 永远返回 Integer.MAX_VALUE |
因为队列无界,不要依赖此方法做容量控制。 |
transfer 可能永久阻塞 |
如果没有消费者调用 take/poll,生产者会一直阻塞。应使用 tryTransfer 或带超时的版本,或设计消费保证。 |
tryTransfer 不保证元素入队 |
返回 false 时元素未被任何线程接收,需要应用层自行处理(如重试、持久化)。 |
poll 和 peek 可能返回 null |
正确判空,避免 NullPointerException。 |
| 公平性带来的性能权衡 | FIFO 公平匹配比 LIFO(如某些栈实现)略慢,但避免了线程饥饿。 |
| 无锁算法导致的调试复杂性 | 内部实现非常复杂,普通开发者不应修改;使用时只需关注 API。 |
元素不可为 null |
与大多数 BlockingQueue 实现一致,插入 null 会抛出 NullPointerException。 |
| 内存可见性由 CAS 保证 | 不需要额外加 volatile 或同步,LinkedTransferQueue 已确保线程安全。 |
8. 与其他阻塞队列的对比总结
| 队列 | 有界性 | 数据结构 | 锁机制 | 支持 transfer | size 复杂度 | 公平性 | 典型应用场景 |
|---|---|---|---|---|---|---|---|
| LinkedTransferQueue | 无界 | 双链表 | 无锁(CAS) | 是 | O(n) | 公平 FIFO | 高吞吐、需要确认送达、缓冲 + handoff 混合 |
| LinkedBlockingQueue | 可选有界(默认无界) | 单链表 | 双锁(take/put 分离) | 否 | O(1) | 非公平(默认) | 传统生产者-消费者缓冲,容量可限制 |
| SynchronousQueue | 0(零容量) | 栈/队列 | 无锁(CAS) | 是(本质 handoff) | O(1) | 可配置(公平/非公平) | 线程池 handoff(如 CachedThreadPool),无缓冲交换 |
| ArrayBlockingQueue | 固定有界 | 数组 | 单锁(ReentrantLock) |
否 | O(1) | 可配置(公平/非公平) | 固定大小缓冲区,资源受限场景 |
补充说明:
LinkedTransferQueue的size()复杂度为 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。