2622 字
约 8 分钟
16
S05-X08 · Disruptor 的 Ring Buffer:无锁队列怎么做到百万级吞吐
2024-12-31
2026-11-08

ArrayBlockingQueue 在 8 核机器上跑到几十万吞吐就会开始抖,而 Disruptor 在同机上能稳定高出接近一个数量级。差距不在"用了数组",而在于三件事:没有锁、没有额外对象分配、没有伪共享。这三条都不是玄学,可以逐条指出实现位置。

这篇按"队列慢在哪 → Ring Buffer 怎么编号 → 序号怎么同步 → 依赖图怎么排 → API 怎么写 → 什么时候不该用"的顺序拆。


1. ArrayBlockingQueue 慢在哪

它把生产者和消费者放在同一把独占锁下:

public void put(E e) throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) notFull.await();   // 满则等待
        enqueue(e);
    } finally { lock.unlock(); }
}
代价 来源 后果
锁竞争 生产/消费共用 ReentrantLock 核多了反而更慢
线程挂起 notFull.await() / 条件队列 上下文切换,微秒级起跳
伪共享 head、tail、count 相邻字段 两个 CPU 反复使对方的缓存行失效
对象分配 每个入队元素背后一个 Node 高吞吐下 GC 压力明显

伪共享最隐蔽:head 被消费者写、tail 被生产者写,落在同一条 64 字节缓存行上时,两个核会不停把这行在缓存间来回抢。JDK 8 的 @sun.misc.Contended 主要用在 Striped64、Thread 这类热点字段上,队列本身并没有;JDK 9 后注解成内部 API,普通代码用不上。

2. Ring Buffer 的序号机制

Ring Buffer 是一个启动时一次性分配好的数组,容量必须是 2 的幂。它不存"第几个元素",而是靠一个只增不减的序号定位:

long sequence = ringBuffer.next();          // 申领序号
try {
    Event e = ringBuffer.get(sequence);     // 按序号取下标处的预分配对象
    e.setValue(sequence);
} finally {
    ringBuffer.publish(sequence);           // 发布,消费者才可见
}

// 序号到下标的换算
int index = (int) sequence & (bufferSize - 1);   // 位运算替代取模
设计 收益
容量为 2 的幂 & (size-1) 替代取模,一条指令
事件对象预分配 稳态零分配,GC 压力接近 0
序号单调递增 用 long 比较判断可用性,天然解决环绕问题
生产与消费分离序号 不需要同一把锁

预分配带来一个容易踩的约束:Ring Buffer 里事件对象的字段会被反复覆盖,消费者拿到的是同一批对象。想留存数据必须自己复制出去,否则读到的是后续写入的值。

3. CAS 与内存屏障

序号的可见性不靠锁,靠 Unsafe/VarHandle 的原子操作与有序写。

// 多生产者:用 CAS 抢序号
protected final boolean casProducerCursor(long expected, long next) {
    return UNSAFE.compareAndSwapLong(this, CURSOR_OFFSET, expected, next);
}

// 单生产者:不需要 CAS,但发布要保证顺序
protected final void publish(long sequence) {
    UNSAFE.putOrderedLong(this, CURSOR_OFFSET, sequence);   // store-store,不刷缓存但保序
}
操作 语义 用在哪
compareAndSetLong 全屏障,失败可重试 多生产者抢序号
putOrderedLong(lazySet) 只保证写顺序 单生产者发布序号
volatile 读写 全屏障 自旋检查与读 cursor

putOrderedLong 是这里的关键优化:事件的字段已经写好(普通写),发布序号只需要保证"先写数据、后写序号"的顺序,不需要立刻把整条缓存行推给其他核。这一步在单生产者场景下省掉了大量内存屏障指令。

4. SequenceBarrier 与依赖图

消费者拿到的不只是一个游标,而是一组"我依赖谁"的信息。SequenceBarrier 负责回答"下一个可处理的序号是多少":

long next = barrier.waitFor(processed + 1);   // 等依赖的序号都推进到该值
EventHandler[] handlers = ...;
// 消费者 D 依赖 A、B、C:D 的 barrier 里挂着这三个 sequence
概念 作用
Sequence 每个消费者自己的进度(CAS 递增,padding 防伪共享)
SequenceBarrier 组合多个依赖序号,提供 waitFor
gatingSequences 生产者反向追踪"最慢的消费者",用于判断环是否满
菱形依赖图 多级处理链,同层可并行、跨层串行

这才是它真正的价值:不只是队列,而是可编排的处理链。等待关系显式声明,不需要中间队列搬数据,事件在同一份内存上被多个处理器依次改写。

5. 生产者模式:单生产者与多生产者

模式 创建方式 抢序号 适用
单生产者 RingBuffer.createSingleProducer(...) 无 CAS,直接读 cursor+1 一个线程投递(最常见)
多生产者 RingBuffer.createMultiProducer(...) CAS 抢序号,失败重试 多个线程投递

单生产者下投递路径只剩"读 cursor → 写字段 → 有序写 cursor",全是本地操作;多生产者多了 CAS 争用,吞吐下降但通常仍优于加锁队列。选错模式是安全性问题:多线程投递却用单生产者 API,序号会被覆盖。

6. 等待策略:延迟与 CPU 的兑换

消费者等待新事件的方式直接决定延迟与 CPU 占用:

策略 做法 延迟 CPU
BusySpinWaitStrategy 死循环自旋 最低 占满一个核
YieldingWaitStrategy 自旋若干次后 Thread.yield() 低 高
SleepingWaitStrategy 失败后先自旋再 LockSupport.parkNanos(1) 中 低
BlockingWaitStrategy ReentrantLock + 条件变量 高(微秒级抖动) 最低
TimeoutBlockingWaitStrategy 带超时的阻塞 中高 低
LiteBlockingWaitStrategy 轻量阻塞 中 低

原则:延迟敏感场景用 Yielding/BusySpin,并接受它吃掉一两个核;通用总线用 Sleeping/Blocking。 用 BusySpin 却只给 1 个 CPU,消费者自旋会把生产者挤死——"换了反而更慢"多半是这个原因。

7. 伪共享:为什么要在字段间塞无用字段

Sequence 类的实现里塞了大量 long 占位:

abstract class RingBufferPad { protected long p1, p2, p3, p4, p5, p6, p7; }        // 左侧填充
abstract class RingBufferFields<E> extends RingBufferPad { /* 数据字段 */ }
public final class RingBuffer<E> extends RingBufferFields<E> { protected long p1..p7; } // 右侧填充

目的只有一个:让 cursor 独占缓存行,不与相邻字段共享。左侧 7 个 long 加右侧 7 个 long,正好各占 56 字节以上,把 64 字节的缓存行撑开。

手段 适用 注意
手写 padding 字段 任何 JDK 代码丑,但最可靠
@Contended(jdk.internal.vm.annotation) JDK 8+ JDK 17 上是内部 API,需 --add-exports(搭配 -XX:-RestrictContended)
对象分离 通用做法 把热字段放进独立对象,靠对象布局天然隔开

JDK 17 上想用 @Contended 相当麻烦:注解在 java.base 内部的 jdk.internal.vm.annotation 包里,普通模块要 --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED 才能引用,且默认受 RestrictContended 限制。Disruptor 自带 padding 类,正是为了避开这个坑。

8. API 骨架

// 1. 事件与工厂:事件对象会被复用,构造器不要写业务初始化
public class LongEvent { private long value; public void set(long v) { this.value = v; } }
EventFactory<LongEvent> factory = LongEvent::new;

// 2. 消费者
EventHandler<LongEvent> handler = (event, sequence, endOfBatch) ->
        System.out.println("seq=" + sequence + " value=" + event.value);

// 3. Ring Buffer + 等待策略(容量必须是 2 的幂)
int bufferSize = 1024;
RingBuffer<LongEvent> ringBuffer = RingBuffer.createSingleProducer(
        factory, bufferSize, new YieldingWaitStrategy());

// 4. 用 Disruptor 门面组装处理链
Disruptor<LongEvent> disruptor = new Disruptor<>(factory, bufferSize,
        DaemonThreadFactory.INSTANCE, ProducerType.SINGLE, new BlockingWaitStrategy());
disruptor.handleEventsWith(handler);
disruptor.handleEventsWith(handlerA, handlerB)   // 同层并行
         .then(handlerC);                        // 下一层依赖 A、B
disruptor.start();

// 5. 投递
ringBuffer.publishEvent((event, sequence) -> event.set(sequence));

五个组件各有各的坑:工厂里做重初始化、handler 抛异常让 BatchEventProcessor 停摆、容量写成非 2 的幂、手动 get(sequence) 越界、等待策略与 CPU 配额不匹配。

EventHandler 抛异常时消费者线程默认会终止,链路静默停摆而生产者还在写。要在 handler 内兜住,或配 ExceptionHandler。

9. 量级与适用边界

公开基准给出的量级:单生产者单消费者、纯内存事件,Disruptor 在千万次/秒级别,ArrayBlockingQueue 通常低一个数量级;BusySpin 下尾部延迟可压到亚微秒。数字随机器与事件大小变化很大,只在自己环境比才有意义。

场景 Disruptor 合适吗 理由
低延迟事件处理(撮合、行情) 合适 延迟敏感 + 单机高吞吐
日志异步落盘(Log4j2) 合适 生产者多、消费串行、可丢弃策略
通用任务队列 不合适 没有动态扩容、没有优先级语义
大量阻塞 IO 任务 不合适 消费者不该阻塞,会拖垮整条链
需要可靠投递/持久化 不合适 环内数据在内存,进程挂即丢

它最容易被误用的地方是当"高性能线程池队列":Disruptor 的消费者是固定线程、串行推进的,任务耗时不均会直接堵塞后续事件;有阻塞 IO 的场合,它比 ThreadPoolExecutor 差得多。

10. 排错:五种常见现象

现象 原因 处理
消费者完全不工作 handler 抛异常导致线程退出 加 ExceptionHandler,或 handler 内兜异常
生产者长期阻塞在 next() 环满且消费者未推进 查消费者是否卡在阻塞 IO
吞吐远低于预期 队列容量太小、等待策略不匹配 加大容量,换 Yielding
CPU 100% 但吞吐不高 BusySpin 空转 换 Sleeping 或 Blocking
数据错乱 / 序号重复 多线程投递却用单生产者 API 改 createMultiProducer

11. 自测

  1. ArrayBlockingQueue 的四个开销分别来自哪里?
  2. Ring Buffer 容量为什么必须是 2 的幂?
  3. putOrderedLong 与 compareAndSetLong 在语义上差在哪?
  4. SequenceBarrier 解决的是什么问题?
  5. BusySpinWaitStrategy 换来的延迟代价是什么?
  6. 为什么 Disruptor 不适合当通用任务队列?
S05-X08 · Disruptor 的 Ring Buffer:无锁队列怎么做到百万级吞吐
https://www.clxhxhhr.top/posts/5661/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。