16.JCTools并发工具
总结
- JCTools 是专为 JVM 设计的高性能并发工具库,弥补了 JDK 并发队列在极端场景下性能不足的问题
- 提供四种非阻塞队列:Spsc、Mpsc、Spmc、Mpmc,按生产者/消费者数量选型
- Netty 内部大量使用 MpscQueue,用于 EventLoop 的任务队列
- MpscQueue 通过填充 Cache Line 解决 CPU 伪共享问题,避免无效的缓存失效
1. 为什么需要 JCTools?
JDK 自带的非阻塞并发队列(如 ConcurrentLinkedQueue)在高并发场景下性能并不够出色,可选择面也少。
JCTools 是专为 JVM 并发开发设计的开源工具库,提供了一批 JDK 缺失的高性能并发数据结构,包括非阻塞 Map、非阻塞 Queue 等。核心优势是针对特定的生产者/消费者模型做了专项优化,在明确使用场景的前提下,性能远超 JDK 通用实现。
2. 四种队列类型
JCTools 的非阻塞队列按生产者和消费者的数量分为四种:
| 队列类型 | 全称 | 生产者 | 消费者 | 典型场景 |
|---|---|---|---|---|
Spsc |
Single Producer Single Consumer | 1 个 | 1 个 | 单线程生产、单线程消费的管道 |
Mpsc |
Multi Producer Single Consumer | 多个 | 1 个 | 多线程提交任务、单线程处理(Netty EventLoop) |
Spmc |
Single Producer Multi Consumer | 1 个 | 多个 | 单线程分发任务、多线程并行消费 |
Mpmc |
Multi Producer Multi Consumer | 多个 | 多个 | 通用多线程场景 |
选型原则:能用约束更强的就不用通用的。比如确定只有一个消费者,就用 Mpsc 而不是 Mpmc,约束越强,能做的优化越多,性能越好。
3. Netty 中的 MpscQueue
Netty 的 NioEventLoop 任务队列用的就是 MpscChunkedArrayQueue:
- 多生产者:业务线程可以随时向 EventLoop 提交任务(
eventLoop.execute(task)) - 单消费者:EventLoop 本身是单线程,只有它自己消费任务队列
这个场景完美匹配 Mpsc 模型,既保证了多线程提交的线程安全,又避免了消费端的锁竞争。
// Netty 中创建 MpscQueue
private final Queue<Runnable> taskQueue = newTaskQueue(maxPendingTasks);
protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
// 使用 JCTools 的 MpscChunkedArrayQueue
return maxPendingTasks == Integer.MAX_VALUE
? PlatformDependent.<Runnable>newMpscQueue()
: PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
}
4. MpscQueue 如何解决 CPU 伪共享?
MpscQueue 在性能优化上做了一个关键处理:填充 Cache Line,解决 CPU 伪共享问题。
关于伪共享的原理见 CPU中的伪共享。简单说就是:多个线程访问不同变量,但这些变量恰好在同一个 Cache Line 里,一个线程修改自己的变量会导致其他线程的缓存失效,产生不必要的缓存同步开销。
MpscQueue 的解决方式是在关键变量前后各填充 7 个 long,强制让该变量独占一个 Cache Line(64 字节):
public class FalseSharingPadding {
protected long p1, p2, p3, p4, p5, p6, p7; // 前置填充:7 × 8 = 56 字节
protected volatile long value = 0L; // 核心变量:8 字节,独占 Cache Line
protected long p9, p10, p11, p12, p13, p14, p15; // 后置填充:7 × 8 = 56 字节
}
这样不论在什么情况下,value 与其他不相关的变量都处于不同的 Cache Line,多线程并发访问时不会互相干扰。
graph LR
subgraph Cache Line 1(64 字节)
A[p1~p7 填充\n56 字节] --- B[value\n8 字节]
end
subgraph Cache Line 2(64 字节)
C[p9~p15 填充\n56 字节] --- D[其他变量]
end5. JCTools 队列 vs JDK 队列
| 对比项 | JDK ConcurrentLinkedQueue | JCTools MpscQueue |
|---|---|---|
| 线程模型 | 通用,任意多生产者多消费者 | 专用,多生产者单消费者 |
| 实现方式 | CAS 链表 | CAS 数组(减少 GC 压力) |
| 伪共享处理 | 无 | Cache Line 填充 |
| 性能 | 通用场景够用 | 高并发下显著更好 |
| 适用场景 | 通用 | 明确单消费者的场景 |
JCTools 的代价是使用约束更强,必须严格遵守生产者/消费者数量的约定,否则会出现线程安全问题。用之前要确认好使用场景。