## 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
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
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`)。
#### 主要流程
1. **尝试匹配等待线程**
从 `head` 开始向后遍历,寻找一个**模式互补**(即 `haveData != node.isData`)的节点。如果找到,尝试通过 CAS 将节点的 `item` 设置为匹配值(生产者把 `item` 从元素改为 `null`,消费者把 `item` 从 `null` 改为元素)。
- CAS 成功:唤醒该节点的等待线程,将匹配节点从链表中断开(`unsplice`),并返回匹配的元素(消费者操作)或 `null`(生产者操作)。
- CAS 失败:说明已被其他线程匹配,继续向后遍历。
1. **如果没有匹配且允许入队**(`how` 为 `ASYNC`/`SYNC`/`TIMED`)
- 创建一个新节点(`isData=haveData`,`item=e`),并尝试将其追加到队尾(CAS 更新 `tail`)。
- 如果 `how` 是 `ASYNC`,直接返回 `null`(生产者的 `put`/`offer` 结束)。
- 如果 `how` 是 `SYNC` 或 `TIMED`,则进入自旋 + 阻塞等待,直到节点被匹配(`item` 被其他线程改变)或超时/中断。
1. **阻塞等待细节**
- 首先进行有限次数的自旋(`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 类图
head/tail
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
Node
-isData : boolean
-item : Object
-next : Node
-prev : Node
-waiter : Thread
+Node(Object item, boolean isData)
**描述**:`LinkedTransferQueue` 持有 `head` 和 `tail` 引用指向双链表节点。每个 `Node` 包含数据/请求标志、元素、前后指针以及等待线程。核心方法 `xfer` 统一处理各种操作模式。
### 4.2 链表结构图
Node3
mode=DATA
item=E2
Node2
mode=REQUEST (消费者)
item=null
Node1
mode=DATA (生产者)
item=E1
head
tail
prev=null
prev=Node1
prev=Node2
prev=Node3
**描述**: 该图展示了一个包含三个节点的双链表实例。
- **`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 核心流程图(匹配阶段)
true 生产者
false 消费者
否
是
失败
成功
xfer 开始
haveData?
期望匹配消费者节点
目标item==null
期望匹配生产者节点
目标item!=null
从head开始扫描
找到互补模式节点?
无匹配,进入入队/返回
CAS 设置节点的item字段
成功?
继续向后扫描
唤醒节点上的等待线程
从链表中移除节点 (unsplice)
返回匹配的值
消费者返回元素,生产者返回null
结束
**描述**: 该图描述了 `xfer` 方法中最关键的**匹配阶段**,即尝试与队列中已存在的等待线程进行“手递手”传递。
1. **确定目标节点类型**:
- 生产者(`haveData=true`)希望找到一个消费者节点,该节点的 `item` 当前为 `null`。
- 消费者(`haveData=false`)希望找到一个生产者节点,该节点的 `item` 不为 `null`(携带具体元素)。
1. **扫描策略**:从 `head` 开始,沿着 `next` 指针遍历链表,直到遇到 `null`(链表末尾)。
- 之所以从 `head` 开始,是为了保证 **FIFO 公平性**:等待时间最长的节点(最靠近 `head`)优先被匹配,避免线程饥饿。
1. **匹配尝试**:对于每个遍历到的节点,检查其 `isData` 是否与当前操作互补(即 `isData != haveData`)。
- 如果互补,则通过 `CAS` 尝试修改该节点的 `item` 字段:
- 生产者将消费者的 `item` 从 `null` 改为非 `null`(实际元素值)。
- 消费者将生产者的 `item` 从非 `null` 改为 `null`。
- CAS 成功意味着当前线程赢得了匹配权;如果失败(说明另一个线程已经抢先匹配了该节点),则继续向后扫描。
1. **匹配成功后的动作**:
- 唤醒该节点上阻塞的线程(`LockSupport.unpark(waiter)`)。
- 调用 `unsplice` 将匹配节点从双链表中移除,更新 `head` 或前后节点的指针。
- 返回相应的值:消费者返回取到的元素,生产者返回 `null`(表示传递成功)。
1. **未匹配到任何节点**:则进入入队逻辑(`how` 决定是否入队),或者对于 `NOW` 模式直接返回。
这个匹配过程是无锁的,多个线程可以同时尝试匹配不同的节点,CAS 保证了原子性。
### 4.4 入队与阻塞流程
ASYNC
NOW
SYNC
TIMED
是
否
否
是
匹配
超时/中断
xfer 无匹配
how 的值?
创建节点,追加到队尾
立即返回
立即返回 null/false
创建节点,追加到队尾
创建节点,追加到队尾
有限次自旋
检查是否被匹配
被匹配?
清理节点,返回
自旋次数耗尽?
LockSupport.park(this)
被唤醒或中断
匹配或超时/中断?
根据情况抛出异常或返回false
结束
**描述**:当匹配阶段未找到互补节点时,根据操作模式 `how` 决定后续行为。
- **`ASYNC` 模式**(`put`、`offer`):
创建一个新节点(`isData=haveData`,`item=e`),通过 CAS 追加到 `tail` 后面,然后立即返回。因为队列无界,永远不会阻塞。
- **`NOW` 模式**(`poll`、`tryTransfer`):
直接返回 `null`(消费者)或 `false`(生产者),**不会创建任何节点**。这体现了“非阻塞、仅尝试”的语义。
- **`SYNC` 模式**(`take`、`transfer`)和 **`TIMED` 模式**(带超时的 `poll` 或 `tryTransfer`):
1. **创建并入队**:与 `ASYNC` 类似,先创建节点并追加到队尾。此时节点处于“等待匹配”状态。
1. **自旋等待**:为了避免昂贵的线程阻塞/唤醒开销,线程会先进行有限次数的**自旋**(`spins`,通常基于 CPU 核数动态计算)。自旋期间反复检查节点的 `item` 是否已被其他线程修改(即是否被匹配)。
1. **阻塞**:如果自旋次数耗尽仍未匹配,则调用 `LockSupport.park(this)` 将当前线程挂起。`TIMED` 模式会使用 `parkNanos` 并设置超时。
1. **唤醒与检查**:当被匹配线程唤醒(或超时/中断)后,再次检查节点状态。如果 `item` 已被修改(匹配成功),则调用 `cleanup` 将节点从链表中移除并返回;否则根据超时或中断标志抛出 `InterruptedException` 或返回 `false`。
自旋机制是性能优化的关键:在锁竞争不激烈或临界区极短的情况下,自旋能避免线程进入内核态,大幅降低延迟。
### 4.5 transfer 与 take 配对时序图
生产者线程LinkedTransferQueue消费者线程生产者阻塞transfer(e)xfer(e, true, SYNC)扫描head,无消费者节点创建数据节点,追加到tail自旋后 park()take()xfer(null, false, SYNC)从head扫描,找到生产者节点CAS 将节点item设为null唤醒生产者线程返回元素e解除阻塞清理节点transfer返回生产者线程LinkedTransferQueue消费者线程
**描述**:该时序图展示了典型的生产者-消费者通过 `transfer` 和 `take` 完成一次“握手”的全过程。
1. **生产者调用 `transfer(e)`** :
- 内部调用 `xfer(e, true, SYNC, 0)`。
- 匹配阶段扫描链表,发现没有等待的消费者节点(队列可能为空或全是生产者节点)。
- 创建一个新的数据节点(`isData=true`, `item=e`),通过 CAS 追加到 `tail`。
- 由于 `how=SYNC`,节点入队后,生产者线程进入自旋,随后调用 `park()` 阻塞。
1. **消费者调用 `take()`** :
- 内部调用 `xfer(null, false, SYNC, 0)`。
- 匹配阶段从 `head` 开始扫描,找到了生产者节点(`isData=true` 且 `item=e`),两者互补。
- 消费者通过 CAS 将该节点的 `item` 从 `e` 修改为 `null`。CAS 成功,表示消费者赢得了匹配权。
- 消费者唤醒生产者节点中保存的 `waiter` 线程(即生产者线程)。
- 消费者返回被取出的元素 `e`,`take()` 结束。
1. **生产者被唤醒**:
- 从 `park()` 返回,检查到节点的 `item` 已被改为 `null`(匹配成功)。
- 调用清理逻辑将节点从链表中移除。
- `transfer(e)` 返回 `void`,生产者继续执行。
注意:如果消费者在生产者之前调用 `take()`,则角色互换——消费者节点会先入队并阻塞,直到生产者到来并匹配它。整个机制是对称的。
### 4.6 tryTransfer 的快速路径
是
否
tryTransfer e
xfer e, true, NOW, 0
从 head 扫描消费者节点
找到等待消费者?
CAS 匹配节点
唤醒消费者
返回 true
返回 false
元素不入队
结束
**描述**:`tryTransfer` 是 `transfer` 的非阻塞版本,其行为由 `how=NOW` 决定。
- **不会创建节点**:即使用户调用 `tryTransfer(e)` 时没有等待的消费者,元素 `e` **也不会被放入队列**。这与其他 `offer` 方法完全不同。
- **仅尝试匹配已有消费者**:从 `head` 开始扫描,寻找一个消费者节点(`isData=false` 且 `item==null`)。
- 如果找到,则通过 CAS 将该消费者的 `item` 从 `null` 设置为 `e`,唤醒消费者线程,并返回 `true`。
- 如果未找到,立即返回 `false`,元素 `e` 被丢弃(应用层需要自行处理,例如重试、持久化)。
- **不阻塞、不超时**:整个操作是纯 CPU 操作,不会导致线程挂起。
此方法适用于“如果消费者恰好正在等待,就传递;否则放弃”的场景,例如在实时系统中避免生产者因等待而阻塞。
### 4.7 多线程并发下的匹配与出队示意图
新生产者C到达
初始队列
模式互补
新生产者C
调用put
与消费者节点A匹配
head
生产者节点B
等待
tail
匹配?
移除节点A
head指向原N2
最终队列
head->生产者节点B
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)
**场景**:消息发送者必须确保消息被接收后才继续发送下一条,保证可靠交付。
```
java
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
生产者开始 transfer...
消费者收到: 重要消息
消费者处理完成
生产者确认消息已被消费,继续执行
```
### 5.2 尝试立即传递(tryTransfer)
**场景**:如果消费者未就绪,生产者立即放弃并执行备选逻辑(如记录日志、暂存数据库)。
```
java
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
无消费者等待,消息未送达,转存至数据库
消费者收到: 第二条消息
第二次尝试结果: true
```
### 5.3 带超时的 tryTransfer
**场景**:生产者等待消费者一段时间,若无人接收则回退。
```
java
体验AI代码助手
代码解读
复制代码
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);
}
}
```
**输出**: [0](http://010-kfp.wikidot.com/) [1](http://021-kfp.wikidot.com/) [2](http://022kaifapiao.wikidot.com/) [3](http://023kaifapiao.wikidot.com/) [4](http://0351kaifapiao.wikidot.com/) [5](http://0471kaifapiao.wikidot.com/) [6](http://0311kaifapiao.wikidot.com/) [7](http://024kaifapiao.wikidot.com/) [8](http://0431kaifapiao.wikidot.com/) [9](http://0451kaifapiao.wikidot.com/)
```
makefile
体验AI代码助手
代码解读
复制代码
1秒内无消费者,消息进入备用队列
第二次尝试结果: true
消费者收到: 延迟消息
```
### 5.4 生产者-消费者缓冲模式(对比 LinkedBlockingQueue)
**场景**:大量数据生产消费,对比 `LinkedTransferQueue` 和 `LinkedBlockingQueue` 的吞吐量(本示例仅展示使用方式,实际性能测试需要更严谨的基准)。
```
java
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
等待消费者数: 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 性能调优建议
1. **避免频繁调用 `size()`** :该方法需要遍历整个链表,时间复杂度 O(n),且返回值是弱一致的(不代表精确大小)。可使用 `isEmpty()` 判断空。
1. **谨慎使用 `transfer`**:如果消费者速度跟不上,生产者会永久阻塞,容易导致线程池耗尽。建议配合超时或使用 `tryTransfer`。
1. **对于纯缓冲场景**:使用 `put`/`take` 即可,无需启用 transfer 语义。
1. **合理设置并发度**:虽然是无锁队列,但过多的线程同时竞争同一个 `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`。
共同学习,写下你的评论
评论加载中...
作者其他优质文章