一、为什么需要 JCTools?

JDK 内置的并发队列存在两个根本性问题:

  1. GC 压力:ConcurrentLinkedQueue 每个节点都是一个独立对象,高吞吐下产生海量垃圾。
  2. 伪共享:JDK 的队列没有做缓存行填充,多线程修改头尾指针时互相踩踏缓存行。

JCTools 的答案是:数组 + 预分配 + 缓存行填充 + 针对性生产者/消费者模型

它不追求万能,而是为每种线程组合模型提供专用队列:


模型典型场景代表队列
SPSC单线程写日志,异步落盘SpscArrayQueue
MPSCNetty 的 EventLoopMpscArrayQueue
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字节]

这样 buffermask 字段被隔离在独立的缓存行中。当多线程访问队列的不同部分时,不会因伪共享导致缓存行反复失效。



四、JCTools 与 Disruptor 的对比


维度DisruptorJCTools
定位完整的事件处理框架纯粹的并发数据结构
依赖关系支持多消费者编排(菱形/顺序)无,就是队列
批处理内置 BatchEventProcessor需自行封装 poll 循环
去GCRingBuffer 预填充事件对象只保证队列本身无GC,元素仍可能产生垃圾
复杂度较重,理解成本高轻量,像用集合一样简单
生态金融交易系统Netty、Reactor、Vert.x

一句话总结:Disruptor 给你的是整个舞台,JCTools 给你的是最锋利的那把刀。



五、实战建议

  1. 线程模型匹配是第一原则:如果你是 Netty 那样的 MPSC 模型,用 MpscArrayQueue,性能是 ConcurrentLinkedQueue 的 5-10 倍。
  2. 容量固定优于扩容:SpscArrayQueue 比 SpscUnboundedArrayQueue 快,如果场景允许,设定合理上界。
  3. 批量消费自己造:JCTools 的 drain 方法支持批量消费:javaqueue.drain(e -> process(e), batchSize);能很大程度上弥补批处理的性能差距。
  4. 配合 VarHandle(JDK 9+):最新版 JCTools 已迁移到 VarHandle,性能更好,不再依赖 Unsafe。


JCTools 之所以成为 Netty 等顶级框架的基石,就是因为它用最简单的数组,配合对现代 CPU 缓存体系的深刻理解,将并发队列的性能推到了理论极限。它不是魔法,是计算机体系结构的诚实映射。