在高并发场景下,传统队列往往是整个系统的性能瓶颈。无论是LinkedBlockingQueue还是ArrayBlockingQueue,它们都依赖锁来保证线程安全,在激烈竞争下会导致大量线程上下文切换和缓存失效。LMAX交易所开源的Disruptor框架给出了另一条路:用环形数组RingBuffer配合无锁算法,单线程每秒可处理数百万级事件,延迟控制在微秒甚至纳秒级别。理解它的设计思想,对写出高性能Java程序有直接帮助。

一、传统阻塞队列的性能瓶颈在哪里
要理解Disruptor的价值,先得看清传统队列慢在哪。以ArrayBlockingQueue为例,它内部用一把ReentrantLock同时保护入队和出队操作,生产者和消费者之间会互相竞争同一把锁。即使改用LinkedBlockingQueue的双锁设计,锁竞争依然存在,而且链表节点在内存中不连续,CPU缓存命中率天然偏低。
锁带来的代价不只是等待。当线程竞争锁失败时会进入阻塞状态,内核将线程挂起再唤醒,一次上下文切换的开销在微秒量级,而一次CAS操作的耗时只有几纳秒,两者相差三个数量级。此外,链式结构的每个节点都是独立分配的对象,可能分散在堆的不同区域,GC压力和缓存缺失会随流量增长而放大。
Disruptor针对这三个问题分别给出解法:用预先分配好的环形数组消除GC压力和内存不连续问题,用CAS和内存屏障替代锁,用缓存行填充消除伪共享。下面逐个展开。
二、RingBuffer环形数组的核心设计
RingBuffer是Disruptor的心脏,本质是一个固定大小的对象数组,在RingBuffer创建时就把所有槽位的对象一次性分配好,之后生产者只做覆盖写入,不创建新对象。这一点非常关键:事件对象在整个生命周期内被反复复用,几乎不会产生垃圾,避免了GC停顿对延迟的干扰。
索引推进采用取模方式。假设数组长度为1024,序号seq递增,对应的数组下标就是seq & (1024 - 1)。因为长度强制为2的n次方,位运算可以替代昂贵的取模运算。序号本身是long类型,即使每秒处理十亿个事件,也要几百年才会溢出,所以不用担心回绕问题。
public void publishEvent() {
// 生产者申请下一个序号,内部通过CAS保证多生产者安全
long next = ringBuffer.next();
try {
// 取到该序号对应槽位的事件对象,直接覆盖字段
OrderEvent event = ringBuffer.get(next);
event.setValue(System.nanoTime());
} finally {
// 发布该序号,消费者通过屏障感知到数据可见
ringBuffer.publish(next);
}
}这段代码体现了Disruptor的典型用法:next()申请序号,业务代码只是修改已有对象的字段,最后publish()发布。写入顺序被一个写屏障保护,保证消费者看到发布序号时,事件内容一定已经写入完成。相比锁方案,整个路径上没有任何系统调用。
三、伪共享问题与消除方案
伪共享是理解Disruptor源码绕不开的概念。CPU缓存不是按单个变量加载的,而是以缓存行为单位,通常一行64字节。假设两个线程分别修改两个不同的long变量,如果这两个变量恰好落在同一个缓存行里,它们在逻辑上毫无冲突,但硬件层面会互相使对方的缓存行失效,性能可能下降几十倍。这就是伪共享:共享不是程序层面的,而是硬件层面的。
在RingBuffer的场景里,生产者的写入游标和消费者的读取游标是两个被高频修改的long值,如果它们相邻,就会形成典型的伪共享。解决方案是在它们周围填充无意义的字段,把真正有效的变量独占一个缓存行。
public class PaddedValue {
// 前置填充 7 个 long,共 56 字节
public long p1, p2, p3, p4, p5, p6, p7;
// 真正被高频修改的值,独占缓存行剩余的 8 字节
public volatile long value;
// 后置填充,防止与下一个对象相邻时产生伪共享
public long q1, q2, q3, q4, q5, q6, q7;
}JDK 8之后有了更简洁的方式,直接使用@jdk.internal.vm.annotation.Contended注解,JVM会在被标注字段前后自动填充。需要注意该注解默认只对JDK内部类生效,业务代码要启用需要加JVM参数-XX:-RestrictContended。Disruptor早期版本靠手动填充,新版部分场景已改用注解,两种方式效果等价,手动填充的好处是不依赖JVM参数,可移植性更强。
伪共享消除后的收益非常直观。在四个生产者四个消费者的压测中,处理同样数量的事件,消除伪共享前后的吞吐差距可以达到两到三倍。这类问题在压测报告中通常表现为CPU占用很高但吞吐上不去,属于典型的隐形性能杀手。
四、完整的生产者消费者示例
下面给出一个可以运行的完整例子,展示从定义事件、构建Disruptor到启动消费者的全流程。引入依赖时建议使用3.x系列,它基于JDK 8以上的LongAdder等工具做了优化。
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import java.util.concurrent.Executors;
public class DisruptorDemo {
// 事件对象,字段会被反复覆盖写入
static class OrderEvent {
long orderId;
}
// 消费者:处理单个事件
static class OrderHandler implements EventHandler<OrderEvent> {
@Override
public void onEvent(OrderEvent event, long sequence, boolean endOfBatch) {
System.out.println("消费事件: " + event.orderId);
}
}
public static void main(String[] args) {
// 三个关键参数:事件工厂、缓冲区大小(必须是2的n次方)、线程工厂
Disruptor<OrderEvent> disruptor = new Disruptor<>(
OrderEvent::new,
1024 * 1024,
Executors.defaultThreadFactory(),
ProducerType.MULTI, // 多生产者模式
new com.lmax.disruptor.BlockingWaitStrategy()
);
disruptor.handleEventsWith(new OrderHandler());
disruptor.start();
RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();
for (int i = 0; i < 10; i++) {
long seq = ringBuffer.next();
try {
OrderEvent e = ringBuffer.get(seq);
e.orderId = i;
} finally {
ringBuffer.publish(seq);
}
}
disruptor.shutdown();
}
}几个参数值得注意。缓冲区大小要根据内存预算和峰值流量设置,太大浪费内存,太小会让生产者频繁等待。ProducerType标明单生产者还是多生产者,单生产者模式下Disruptor可以省掉CAS,直接用普通写加内存屏障,性能更高,因此如果业务上只有一个写入线程,务必声明ProducerType.SINGLE。
等待策略也直接影响延迟特征:BlockingWaitStrategy利用锁和条件变量,CPU消耗最低但延迟抖动大;BusySpinWaitStrategy忙等,延迟最低但占满CPU;YieldingWaitStrategy介于两者之间,先自旋再让出时间片。日志、行情推送这类对延迟敏感的场景常用自旋类策略,而普通业务系统用阻塞策略更省资源。
五、实践中的几点建议
第一,事件对象里尽量放基本类型或可变字段,不要每次set新的引用对象,否则复用机制带来的免GC优势会被抵消。第二,消费者链路可以通过handleEventsWith和then构建流水线,让多个阶段并行执行,这是Disruptor相对传统队列的独特能力。第三,监控上要关注发布序号与消费者序号的差值,差值逼近缓冲区大小说明消费者跟不上了,需要扩容消费者或调大RingBuffer。
总的来说,Disruptor的快不是单点技巧,而是一套组合拳:预分配的连续内存、无锁的序号协调、精确到缓存行的内存布局。理解了这三层,再看Netty、Log4j2等框架对Disruptor的借鉴,就会有豁然开朗的感觉。在自己的系统里,遇到队列吞吐不足或延迟毛刺问题时,不妨把这套思路移植过来。
DisruptorRingBuffer伪共享修改时间:2026-09-15 12:28:46