一、为什么需要 JCTools?
JDK 内置的并发队列存在两个根本性问题:
- GC 压力:ConcurrentLinkedQueue 每个节点都是一个独立对象,高吞吐下产生海量垃圾。
- 伪共享:JDK 的队列没有做缓存行填充,多线程修改头尾指针时互相踩踏缓存行。
JCTools 的答案是:数组 + 预分配 + 缓存行填充 + 针对性生产者/消费者模型。
它不追求万能,而是为每种线程组合模型提供专用队列:
| 模型 | 典型场景 | 代表队列 |
|---|---|---|
| SPSC | 单线程写日志,异步落盘 | SpscArrayQueue |
| MPSC | Netty 的 EventLoop | MpscArrayQueue |
| SPMC | 单生产者广播给多个工作线程 | SpmcArrayQueue |
| MPMC | 通用多线程任务分发 | MpmcArrayQueue |
二、核心设计:一条数组的三种极致
2.1 SPSC:极简即极速
为什么快? 没有竞争,不需要 CAS,只需要正确的内存排序。
// SpscArrayQueue 核心原理简化
public final class SpscArrayQueue<E> {
// 1. 缓存行填充,prod/cons 互不干扰
// 2. 生产者只写,消费者只读 —— 绝对的单线程绑定
}生产者 offer 的核心逻辑:
// 简化表示,非真实源码
public boolean offer(E e) {
// 只读消费者索引 —— 单生产者,不需要 CAS
long consumerIndex = lvConsumerIndex(); // 有序读
// 计算下一个可写位置
long currentProducerIndex = producerIndex;
long nextOffset = calcElementOffset(currentProducerIndex);
// 满检查:如果追尾消费者,拒绝
if (currentProducerIndex - consumerIndex >= capacity) {
return false;
}
// 先放数据
soElement(buffer, nextOffset, e); // StoreStore 有序写
// 再更新索引 —— 这一行执行前,消费者看不到数据
soProducerIndex(currentProducerIndex + 1); // StoreStore
return true;
}消费者 poll 的核心逻辑:
public E poll() {
long producerIndex = lvProducerIndex(); // 有序读
long currentConsumerIndex = consumerIndex;
// 空检查
if (currentConsumerIndex >= producerIndex) {
return null;
}
long offset = calcElementOffset(currentConsumerIndex);
// 先取数据
E e = lpElement(buffer, offset); // LoadLoad 有序读
// 再更新索引 —— 这一行执行前,生产者还看不到空位
soConsumerIndex(currentConsumerIndex + 1);
return e;
}关键点:
- soProducerIndex 和 soConsumerIndex 底层都是 Unsafe.putOrderedLong,只带 StoreStore 屏障,比 volatile 写代价低很多。
- 生产者和消费者各自拥有独立的索引缓存,它们之间的可见性顺序是通过 putOrderedLong 和 lazySet 来保证的,而不是昂贵的 volatile 全屏障。
2.2 MPSC:CAS 只争一次
多个生产者并发 offer,只有一个消费者 poll。
生产者 offer 的关键:
// 多生产者需要竞争写入权 —— 一次 CAS
public boolean offer(E e) {
long producerIndex;
do {
producerIndex = lvProducerIndex();
// 检查是否满
if (producerIndex - lvConsumerIndex() >= capacity) {
return false;
}
} while (!casProducerIndex(producerIndex, producerIndex + 1));
// ↑ 只用一次 CAS 抢占序号
// 拿到自己的 slot 后,直接写入,不竞争
soElement(buffer, calcElementOffset(producerIndex), e);
return true;
}设计精妙之处:
- 每个生产者只是通过 CAS 预约一个位置,然后把数据写入自己独占的那个 slot,与其他生产者没有二次竞争。
- 这比 ConcurrentLinkedQueue 每次 casNext 的开销更集中、更可预测。
消费者的挑战:
消费者不能简单地按生产者索引读取,因为可能存在“空洞”:生产者 A 预约了 slot 7,但在写完数据前,slot 7 还是空的。JCTools 的 MpscArrayQueue 通过额外的“版本号”或“状态标记”数组来解决,确保消费者不会读到未写完的数据。
2.3 MPMC:两阶段 CAS
MPMC 是所有模型中最复杂的,每个 slot 需要协调多个生产者和多个消费者的竞争。
核心思路:对数组的每个元素附加一个“序列号”,而不是单纯依赖全局索引。
// 简化原理,展示核心思想
public class MpmcArrayQueue<E> {
// 原子化存储序列号的数组,和对象数组一一对应
// 序列号用于追踪该 slot 的当前"回合"
final AtomicLongArray sequenceBuffer;
public boolean offer(E e) {
long seq, offset;
do {
seq = lvProducerIndex();
offset = calcSequenceOffset(seq); // 对应 slot 的序列号槽位
// 该 slot 是否已经被"回收"(可写)?
if (lvSequence(sequenceBuffer, offset) < seq) {
return false; // 满或竞争失败
}
} while (!casProducerIndex(seq, seq + 1));
// 写入数据
soElement(buffer, calcElementOffset(seq), e);
// 将序列号向前推进一步,允许消费者读取
soSequence(sequenceBuffer, offset, seq + 1);
return true;
}
}每个 slot 的序列号本质是一个“通行证”,生产者和消费者通过争夺这个通行证来协调,全程无锁。
三、缓存行填充:不为人知的性能关键
JCTools 的每个队列,头尾都有大量填充:
// 继承层次结构实现填充(概念展示)
abstract class ConcurrentSequencedCircularArrayQueuePad0 {
long p00, p01, ..., p0e; // 15个 long 填充
}
abstract class ConcurrentSequencedCircularArrayQueueFields extends ... {
long[] buffer;
long mask;
}
abstract class ConcurrentSequencedCircularArrayQueuePad1 extends ... {
long p10, p11, ..., p1e; // 15个 long 填充
}内存布局效果:
[填充 120字节] [buffer, mask] [填充 120字节]这样 buffer 和 mask 字段被隔离在独立的缓存行中。当多线程访问队列的不同部分时,不会因伪共享导致缓存行反复失效。
四、JCTools 与 Disruptor 的对比
| 维度 | Disruptor | JCTools |
|---|---|---|
| 定位 | 完整的事件处理框架 | 纯粹的并发数据结构 |
| 依赖关系 | 支持多消费者编排(菱形/顺序) | 无,就是队列 |
| 批处理 | 内置 BatchEventProcessor | 需自行封装 poll 循环 |
| 去GC | RingBuffer 预填充事件对象 | 只保证队列本身无GC,元素仍可能产生垃圾 |
| 复杂度 | 较重,理解成本高 | 轻量,像用集合一样简单 |
| 生态 | 金融交易系统 | Netty、Reactor、Vert.x |
一句话总结:Disruptor 给你的是整个舞台,JCTools 给你的是最锋利的那把刀。
五、实战建议
- 线程模型匹配是第一原则:如果你是 Netty 那样的 MPSC 模型,用 MpscArrayQueue,性能是 ConcurrentLinkedQueue 的 5-10 倍。
- 容量固定优于扩容:SpscArrayQueue 比 SpscUnboundedArrayQueue 快,如果场景允许,设定合理上界。
- 批量消费自己造:JCTools 的 drain 方法支持批量消费:javaqueue.drain(e -> process(e), batchSize);能很大程度上弥补批处理的性能差距。
- 配合 VarHandle(JDK 9+):最新版 JCTools 已迁移到 VarHandle,性能更好,不再依赖 Unsafe。
JCTools 之所以成为 Netty 等顶级框架的基石,就是因为它用最简单的数组,配合对现代 CPU 缓存体系的深刻理解,将并发队列的性能推到了理论极限。它不是魔法,是计算机体系结构的诚实映射。

