简介
disruptor,一个英国公司开发的组件,高性能无锁队列,是程序内部使用的队列,不是类似于kafka一样的队列。相较于常见的阻塞队列,例如ArrayBlockingQueue,它的性能更高。
disruptor相较于普通的内存队列,做了哪些优化? disruptor是无锁的,在多线程环境下通过cas操作来保证线程安全,并且优化了伪共享的问题。disruptor在设计上,根据计算机底层的硬件原理,来设计算法和数据结构,最大化使用CPU缓存,减少锁竞争和上下文切换的开销
入门案例
案例: 设计一个生产者、一个消费者,生产者向disruptor无锁队列中存放数据,消费者获取数据,随后把数据打印到控制台
第一步:事件对象和事件对象工厂。disruptor的队列实例一但创建,只支持操作指定类型的数据,并且disruptor会预先使用事件对象工厂,创建空事件对象,填充满整个队列
java
// 事件对象
@NoArgsConstructor
@Getter
@Setter
public class LogEvent {
public Long value;
}
// 事件对象工厂
public class LogEventFactory implements EventFactory<LogEvent> {
@Override
public LogEvent newInstance() {
return new LogEvent();
}
}
第二步: 生产者。 生产者持有disruptor中环形缓冲区的实例,向环形缓存区中存放数据时,先获取缓冲区中的序列号,获取序列号处的空对象,填充空对象,再调用环形缓冲区的publish方法
java
public class LogEventProducer {
// 环形缓冲区
private final RingBuffer<LogEvent> ringBuffer;
public LogEventProducer(RingBuffer<LogEvent> ringBuffer) {
this.ringBuffer = ringBuffer;
}
// 生产数据
public void onData(long data) {
// 1、 获取最新的序列号
long sequence = ringBuffer.next();
try {
// 2、 获取序列号处的空对象,向空对象中填充数据
LogEvent logEvent = ringBuffer.get(sequence);
logEvent.setValue(data);
} finally {
// 3、 发布数据
ringBuffer.publish(sequence);
}
}
}
第三步: 消费者。 消费缓冲区中的数据
java
// 消费者实现EventHandler接口。
public class LogEventHandler implements EventHandler<LogEvent> {
// 当前方法的三个参数: 事件对象、序列号、是否队列末尾
@Override
public void onEvent(LogEvent event, long sequence, boolean endOfBatch) throws Exception {
System.out.println("handler1 " +
Thread.currentThread().getName() + " " +
System.currentTimeMillis() +
", event.value = " + event.getValue() +
", sequence = " + sequence +
", endOfBatch = " + endOfBatch);
}
}
第四步: 消费者的线程工厂。 一个线程工厂,用于为消费者创建线程。disruptor使用线程工厂来代替线程池,作为无锁队列的入参,因为线程工厂更加符合disruptor的要求,它会调用用户提供的线程工厂,为每个消费者创建一个线程,如果是线程池,线程迟中的线程数和消费者个数未必对应。用户提供造线程的能力,disruptor负责管理线程的启动、运行、销毁。
java
public class HandlerThreadFactory implements ThreadFactory {
private static final String DEFAULT_THREAD_NAME_PREFIX = "disruptor-handler-";
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public HandlerThreadFactory(String namePrefix) {
this.group = Thread.currentThread().getThreadGroup();
this.namePrefix = namePrefix;;
}
public HandlerThreadFactory() {
this(DEFAULT_THREAD_NAME_PREFIX);
}
public Thread newThread(Runnable r) {
return new Thread(group, r, namePrefix + threadNumber.getAndIncrement());
}
}
// 第五步: 把生产者、消费者组装起来,启动disruptor
java
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
public class Main {
static int BUFFER_SIZE = 1024;
public static void main(String[] args) {
// 环形缓冲区的大小,必须是2的指数
int bufferSize = BUFFER_SIZE;
// 事件工厂
LogEventFactory logEventFactory = new LogEventFactory();
// 线程工厂
HandlerThreadFactory handlerThreadFactory = new HandlerThreadFactory();
Disruptor<LogEvent> disruptor = new Disruptor<>(logEventFactory, bufferSize, handlerThreadFactory);
// 设置事件处理器,这里只有一个
disruptor.handleEventsWith(new LogEventHandler());
// 启动disruptor。这里具体是启动消费者线程,并且当前方法只可以执行一次。
disruptor.start();
// 获取disruptor的环形缓冲区,向缓冲区中生产数据
RingBuffer<LogEvent> ringBuffer = disruptor.getRingBuffer();
// 创建生产者,向环形缓冲区中存放数据
LogEventProducer producer = new LogEventProducer(ringBuffer);
for (int i = 1; i <= BUFFER_SIZE + 1; i++) {
producer.onData(i);
}
// 获取队列中剩余的空间大小,如果剩余大小不等于队列大小,自旋,直到他们相等证明队列中的元素被消费完成,关闭disruptor
long remainingCapacity = ringBuffer.remainingCapacity();
while (remainingCapacity != BUFFER_SIZE) {
remainingCapacity = ringBuffer.remainingCapacity();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
disruptor.shutdown();
}
}
入门案例总结: 这个案例演示了disruptor的基本使用,这里是单生产者、单消费者的使用方式。
基本使用
消息生产者优化为translator
在入门案例中,消息的生产者,在生产消息时,需要获取队列中的下一个序列号,根据序列号获取空对象,向空对象中设置数据,然后发布数据,使用translator来代替producer,可以把上述步骤优化为1步。
第一步: 消息的生产者,这里是translator
java
public class LogEventProducerWithTranslator {
// 提供把数据封装到空对象中的方法,这里是单个参数的translator,因为event中只有一个字段
private static final EventTranslatorOneArg<LogEvent, Long> TRANSLATOR = new EventTranslatorOneArg<LogEvent, Long>() {
@Override
public void translateTo(LogEvent event, long sequence, Long value) {
event.setValue(value);
}
};
private final RingBuffer<LogEvent> ringBuffer;
public LogEventProducerWithTranslator(RingBuffer<LogEvent> ringBuffer) {
this.ringBuffer = ringBuffer;
}
public void onData(long data) {
ringBuffer.publishEvent(TRANSLATOR, data);
}
}
生产者发布数据的方式和之前差不多,获取translator实例,调用onData方法。
使用translator代替producer,可以把发布消息的三步优化为一步,用户只需要提供向空对象中设置数据的方法即可。
单生产者
如果确定只会有一个生产者,创建disruptor实例时可以加一个配置,这样在向队列中添加数据时,会直接更新序列号,不会走cas操作,速度更快
java
Disruptor<LogEvent> disruptor = new Disruptor<>(logEventFactory, bufferSize, handlerThreadFactory,
// 创建disruptor实例时指定只有一个生产者
ProducerType.SINGLE,
// 这里需要指定阻塞策略,就是队列中没有数据时消费者怎么办,
// 这里是指定的是默认的阻塞策略,消费者阻塞等待。
new BlockingWaitStrategy());
多生产者、多消费者
这应该是实际开发中最常见的情况了,这里配置的是,多个生产者同时生产数据,多个消费者同时消费数据,一条数据每个消费者单独消费一次,有几个消费者,一条数据就会被消费几次
案例:
java
public class Main4 {
static int BUFFER_SIZE = 1024;
public static void main(String[] args) {
// 环形缓冲区的大小,必须是2的指数
int bufferSize = BUFFER_SIZE;
// 事件工厂
LogEventFactory logEventFactory = new LogEventFactory();
// 线程工厂
HandlerThreadFactory handlerThreadFactory = new HandlerThreadFactory();
Disruptor<LogEvent> disruptor = new Disruptor<>(logEventFactory, bufferSize, handlerThreadFactory);
// 多消费者
disruptor.handleEventsWith(new LogEventHandler(), new LogEventHandler2(), new LogEventHandler3());
// 多生产者
RingBuffer<LogEvent> ringBuffer = disruptor.getRingBuffer();
Thread thread = new Thread(() -> {
LogEventProducerWithTranslator producer = new LogEventProducerWithTranslator(ringBuffer);
for (int i = 1; i <= 1000; i++) {
producer.onData(i);
}
});
Thread thread1 = new Thread(() -> {
LogEventProducerWithTranslator producer = new LogEventProducerWithTranslator(ringBuffer);
for (int i = 1001; i <= 2000; i++) {
producer.onData(i);
}
});
Thread thread2 = new Thread(() -> {
LogEventProducerWithTranslator producer = new LogEventProducerWithTranslator(ringBuffer);
for (int i = 2001; i <= 3000; i++) {
producer.onData(i);
}
});
// 启动disruptor
disruptor.start();
// 启动生产者
thread.start();
thread1.start();
thread2.start();
try {
thread.join();
thread1.join();
thread2.join();
} catch (Exception e) {
throw new RuntimeException(e);
}
// 关闭disruptor
long remainingCapacity = ringBuffer.remainingCapacity();
while (remainingCapacity != BUFFER_SIZE) {
remainingCapacity = ringBuffer.remainingCapacity();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
disruptor.shutdown();
}
}
消费者之间的顺序
设置消费者之间的消费顺序: then方法
java
disruptor.handleEventsWith(new LogEventHandler(), new LogEventHandler2())
.then(new LogEventHandler3());
多消费者,一条消息只消费一次
多消费者,一条消息只消费一次,这种情况类似于mq中的消费者组,这种消费方式可以增加并发度,提升消费速度。
案例:
java
// 1、 消费者继承WorkHandler,之前继承的是EventHandler
public class LogHandler4 implements WorkHandler<LogEvent> {
@Override
public void onEvent(LogEvent event) throws Exception {
System.out.println("handler4 " +
Thread.currentThread().getName() + " " +
System.currentTimeMillis() +
", event.value = " + event.getValue());
}
}
// 2、 disruptor实例指定消费者组时,和之前使用的方法也不同
disruptor.handleEventsWithWorkerPool(new LogHandler4(), new LogHandler5(), new LogHandler6());
等待策略
当队列中没有消息时,消费者如何等待? 默认的等待策略是阻塞 BlockingWaitStrategy ,此外,还有睡眠、yield、自旋等。 在创建disruptor实例时指定等待策略。
通常默认的等待策略就可以,如果追求极致的性能,可以选择 YieldingWaitStrategy , 如果没有数据,它会执行 Thread.yield ,暂时让出CPU,不过要注意,这种方法如果没有数据要消费,会一直空跑,造成CPU使用率升高。
尝试发布
如果消息队列满了,publish方法默认的行为是休眠1纳秒,然后继续获取队列中的下一个序列号,这种方式,如果下游消费能力不足,上游会一直循环,导致CPU压力变大,disruptor提供了尝试发布的功能,如果队列满了,发布时会抛出异常,用户可以选择捕获这个异常,然后处理,常见的处理方式包括丢弃、存储到第三方组件、直接调用消费者等。只有translator组件才支持这个功能,普通的producer不支持
案例:
java
// translator组件
public class LogEventProducerWithTranslator {
private static final EventTranslatorOneArg<LogEvent, Long> TRANSLATOR = new EventTranslatorOneArg<LogEvent, Long>() {
@Override
public void translateTo(LogEvent event, long sequence, Long value) {
event.setValue(value);
}
};
private final RingBuffer<LogEvent> ringBuffer;
public LogEventProducerWithTranslator(RingBuffer<LogEvent> ringBuffer) {
this.ringBuffer = ringBuffer;
}
public void onData(long data) {
// 尝试发布
ringBuffer.tryPublishEvent(TRANSLATOR, data);
}
}
源码解析
组件架构
uml
@startuml disruptor组件架构图
!pragma layout smetana
top to bottom direction
title disruptor组件架构图
namespace 核心入口 #FF6B6B {
class Disruptor {
- p1, p2, p3, p4, p5, p6, p7: long
}
}
namespace 循环队列 {
class RingBuffer {
- p1, p2, p3, p4, p5, p6, p7: long
}
abstract class RingBufferFields {
- entries: Object[]; // 存储数据的循环队列
- bufferSize: int; // 队列大小
- sequencer: Sequencer; // 循环队列的序列号
}
interface Sequenced {
+ remainingCapacity(): long; // 获取队列的剩余空间
+ next(): long; // 获取下一个序列号
+ tryNext():long; // 尝试获取下一个序列号,会抛出空间不足异常
+ publish(long sequence): void; // 向指定序列号处存储元素
}
}
namespace 序列器 {
class MultiProducerSequencer {
- availableBuffer: int[];
- indexMask: int;
- indexShift: int;
}
abstract class AbstractSequencer {
- cursor: Sequence; // 游标,当前循环队列中最新元素的位置
- gatingSequences: Sequence[]; // 门控序列,消费者的序列号
}
interface Sequencer {
+ isAvailable(long sequence): boolean; // 当前序列号是否可以消费
+ addGatingSequences(Sequence... gatingSequences): void; // 添加 门控序列,把消费者的序列号添加进来
+ removeGatingSequence(Sequence sequence): boolean; // 移除门控 序列
}
}
namespace 消费者 {
class BatchEventProcessor<T> {
- sequence: Sequence; // 消费者的序列号
- sequenceBarrier: SequenceBarrier; // 序列屏障,判断某个序列号是 否可以消费
- eventHandler: EventHandler<? super T>; // 事件处理器,消费队列 中的元素
}
class ProcessingSequenceBarrier {
- cursorSequence: Sequence; // 循环队列的游标
- dependentSequence: Sequence; // 依赖的序列器,实现多个消费者有序消费
- sequencer: Sequencer; // 循环队列
- waitStrategy: WaitStrategy; // 阻塞策略,没有数据时消费者的等待策略
}
}
namespace 序列号器 {
class Sequence {
}
class RhsPadding {
- p9, p10, p11, p12, p13, p14, p15: long;
}
class Value {
- value: volatile long;
}
class LhsPadding {
- p1, p2, p3, p4, p5, p6, p7: long;
}
}
' 实体类之间的继承关系
' 循环队列
RingBuffer --|> RingBufferFields : 继承
RingBuffer --|> Sequenced : 实现
' 序列号器
MultiProducerSequencer --|> AbstractSequencer: 继承
AbstractSequencer --|> Sequencer: 实现
' 序列号
Sequence --|> RhsPadding: 继承
RhsPadding --|> Value: 继承
Value --|> LhsPadding: 继承
' 实体类之间的组合关系
' 核心入口类
Disruptor --> RingBuffer: 核心入口类持有循环队列的实例
RingBuffer --> MultiProducerSequencer: 持有序列器实例
' 消费者
BatchEventProcessor --> ProcessingSequenceBarrier: 持有序列屏障
ProcessingSequenceBarrier --> RingBuffer: 持有循环队列
' 序列号
MultiProducerSequencer --> Sequence: 消费者、循环队列,都持有Sequence的实例
' 注释
note top of Sequence
负责生成序列号,这里就是CPU缓存行伪共享的解决方案,
value字段通过Unsafe类由cas算法更新,它的左侧和右侧
都填充了56个字节
end note
note top of MultiProducerSequencer
<b>availableBuffer</b>
和循环队列等长的int数组,
里面的元素和循环队列中的一一对应,
标识该位置的元素是否可以消费
<b>indexMask</b>
值等于bufferSize - 1,
和序列号进行按位与运算,
序列号长度超过数组长度时,
通过它来回到数组开头
<b>indexShift</b>
以2为底的bufferSize对数,
用于计算序列号对应的圈数:
圈数 = sequence >>> indexShift,
结果存入数组,用于availableBuffer
中标记数据可用性
end note
@enduml
概述:
- 基本结构包括Disruptor入口类、RingBuffer循环队列、Sequencer序列号器、Sequence序列号,
- Disruptor: 核心入口类,负责启动队列
- RingBuffer: 循环队列
- Sequencer: 序列号器,判断某个序列号是否可以消费
- Sequence: 生成序列号
- 消费者: 用户创建的消费者,如果是实现了 EventHandler,会被包装进 BatchEventProcessor ,其它也类似。 一个processor实例,就是一个消费者,消费者有一个自己的sequence,指向自己消费的位置,同时还持有SequenceBarrier,序列屏障,序列屏障持有缓冲区的cursor,序列屏障用户确保消费者的sequence不会超过队列的cursor。用户自己的序列,会被添加到Sequencer的gatingSequences(门控序列)中,用户判断消费者的消费进度
如何向队列中添加元素:
- 获取队列中下一个可用序列号: 调用RingBuffer的next方法,生成下一个序列号: 内部调用sequencer,sequencer内部的cursor加1。
- 判断队列是否已满: 生成序列号时,会和门控序列数组进行比较。门控序列是消费者创建成功后,把自己的序列号器加入到RingBuffer的门控序列数组中,它代表消费者的消费进度。获取数组中的最小值,它代表消费最慢的消费者,例如,循环队列的长度是8,序列号从0开始,递增到7序列就满了,此时,获取下一个序列号,会先判断门控中的最小值,假如最小值是-1,代表消费者还没有开始消费,此时队列是满的,生产者会一直循环投递,直到消费者消费元素,队列中有空位之后。
- 根据序列号,获取缓存队列中的空对象,向空对象中填充数据,然后调用ringBuffer的publish方法
- publish方法: 设置availableBuffer,标识当前序列号可用,然后唤醒消费者。availableBuffer是一个int类型的数组,和循环队列中的元素一一对应。disruptor中序列号是一直递增的,如果序列号超过数组长度,会和数组长度进行按位与运算,计算出序列号对应的数组下标,所以disruptor要求队列长度必须是2的倍数。先是序列号和数组长度按位与运算,计算出数组下标,然后序列号右位移指定长度,把位移结果存入到availableBuffer,标识指定序列号是可以消费的。
消费者如何消费队列中的元素?
- 生产者向ringBuffer中写入数据后,唤醒消费者线程
- 消费者有自己独立的sequence,默认从0开始消费。消费者获取自己当前的sequence,生成下一次要消费的位置,然后和ringBuffer的cursor相比较,如果小于cursor,再判断sequence经过右位移运算之后,和availableBuffer中的值是否一致,如果一直,就是可以消费。
- 如果可以消费,获取队列中的元素
- 消费完所有可消费的元素之后,继续等待
- 消费者的消费进度是实时更新到门控序列数组中
多个消费者如何实现并行消费: 如果消费者实现EventHandler,那么默认多个消费者会消费同一条消息,如果消费者实现WorkHandler,一条消息只会被一个消费者消费,这是如何做到的? 实现了WorkHandler的消费者,会被组织到 WorkPool中,多个消费者共用一个sequence
ArrayBlockingQueue ConcurrentLinkedQueue disruptor 性能对比
设计一个实验,比较这三个队列的性能: 在队列大小固定的情况下,发送n条消息,比较从消息发送到消费完成之间的耗时。
整体的测试配置: 提取公共的代码
java
public class BenchmarkConfig {
// 每个生产者生产消息数
public static final int TIMES = 10_000_000;
// 生产者数量
public static final int PRODUCER_COUNT = 8;
// 总消息数
public static final int TOTAL_MESSAGES = TIMES * PRODUCER_COUNT;
// 队列容量
public static final int QUEUE_SIZE = 4 * 1024 * 1024; // 1M
// LogEvent 实体类
@Data
public static class LogEvent {
private long value;
}
}
1、 ArrayBlockingQueue
java
public class ArrayBlockingQueueBenchmark {
public static long runTest() throws InterruptedException {
// 1. 创建队列
ArrayBlockingQueue<BenchmarkConfig.LogEvent> queue =
new ArrayBlockingQueue<>(BenchmarkConfig.QUEUE_SIZE);
// 2. 计数器
AtomicLong counter = new AtomicLong(0);
CountDownLatch latch = new CountDownLatch(1);
// 3. 创建生产者线程
Thread[] producers = new Thread[BenchmarkConfig.PRODUCER_COUNT];
for (int i = 0; i < BenchmarkConfig.PRODUCER_COUNT; i++) {
final int startIdx = i * BenchmarkConfig.TIMES;
final int endIdx = (i + 1) * BenchmarkConfig.TIMES;
producers[i] = new Thread(() -> {
for (int j = startIdx; j < endIdx; j++) {
BenchmarkConfig.LogEvent event = new BenchmarkConfig.LogEvent();
event.setValue(j);
try {
queue.put(event); // 阻塞式插入
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
// 4. 创建消费者线程
Thread consumer = new Thread(() -> {
while (true) {
try {
BenchmarkConfig.LogEvent event = queue.take(); // 阻塞式获取
long count = counter.incrementAndGet();
if (count == BenchmarkConfig.TOTAL_MESSAGES) {
latch.countDown();
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
// ===== 开始计时 =====
long start = System.currentTimeMillis();
// 5. 启动消费者
consumer.start();
// 6. 启动所有生产者
for (Thread t : producers) {
t.start();
}
// 7. 等待所有生产者完成
for (Thread t : producers) {
t.join();
}
// 8. 等待消费者完成
latch.await();
long end = System.currentTimeMillis();
// 9. 中断消费者(安全退出)
consumer.interrupt();
return end - start;
}
}
2、 ConcurrentLinkedQueue
java
public class ConcurrentLinkedQueueBenchmark {
public static long runTest() throws InterruptedException {
// 1. 创建队列(无界)
ConcurrentLinkedQueue<BenchmarkConfig.LogEvent> queue =
new ConcurrentLinkedQueue<>();
// 2. 计数器
AtomicLong counter = new AtomicLong(0);
CountDownLatch latch = new CountDownLatch(1);
// 3. 创建生产者线程
Thread[] producers = new Thread[BenchmarkConfig.PRODUCER_COUNT];
for (int i = 0; i < BenchmarkConfig.PRODUCER_COUNT; i++) {
final int startIdx = i * BenchmarkConfig.TIMES;
final int endIdx = (i + 1) * BenchmarkConfig.TIMES;
producers[i] = new Thread(() -> {
for (int j = startIdx; j < endIdx; j++) {
BenchmarkConfig.LogEvent event = new BenchmarkConfig.LogEvent();
event.setValue(j);
queue.offer(event); // 非阻塞插入
}
});
}
// 4. 创建消费者线程
Thread consumer = new Thread(() -> {
int emptyCount = 0;
while (true) {
BenchmarkConfig.LogEvent event = queue.poll();
if (event != null) {
long count = counter.incrementAndGet();
if (count == BenchmarkConfig.TOTAL_MESSAGES) {
latch.countDown();
break;
}
emptyCount = 0; // 重置空计数
} else {
// 如果队列为空,短暂让出 CPU
emptyCount++;
if (emptyCount > 100) {
Thread.yield();
emptyCount = 0;
}
}
}
});
// ===== 开始计时 =====
long start = System.currentTimeMillis();
// 5. 启动消费者
consumer.start();
// 6. 启动所有生产者
for (Thread t : producers) {
t.start();
}
// 7. 等待所有生产者完成
for (Thread t : producers) {
t.join();
}
// 8. 等待消费者完成
latch.await();
long end = System.currentTimeMillis();
// 9. 中断消费者
consumer.interrupt();
return end - start;
}
}
3、 disruptor
java
public class DisruptorBenchmark {
public static long runTest() throws InterruptedException {
// 1. 创建 Disruptor
Disruptor<BenchmarkConfig.LogEvent> disruptor = new Disruptor<>(
BenchmarkConfig.LogEvent::new, // 事件工厂
BenchmarkConfig.QUEUE_SIZE, // RingBuffer 大小
DaemonThreadFactory.INSTANCE // 线程工厂
);
// 2. 计数器
AtomicLong counter = new AtomicLong(0);
CountDownLatch latch = new CountDownLatch(1);
// 3. 注册消费者
disruptor.handleEventsWith(new LogEHandler(latch, counter));
// 4. 启动 Disruptor
disruptor.start();
RingBuffer<BenchmarkConfig.LogEvent> ringBuffer = disruptor.getRingBuffer();
// 5. 创建生产者线程
Thread[] producers = new Thread[BenchmarkConfig.PRODUCER_COUNT];
for (int i = 0; i < BenchmarkConfig.PRODUCER_COUNT; i++) {
final int startIdx = i * BenchmarkConfig.TIMES;
final int endIdx = (i + 1) * BenchmarkConfig.TIMES;
producers[i] = new Thread(() -> {
for (int j = startIdx; j < endIdx; j++) {
long sequence = ringBuffer.next();
try {
BenchmarkConfig.LogEvent event = ringBuffer.get(sequence);
event.setValue(j);
} finally {
ringBuffer.publish(sequence);
}
}
});
}
// ===== 开始计时 =====
long start = System.currentTimeMillis();
// 6. 启动所有生产者
for (Thread t : producers) {
t.start();
}
// 7. 等待所有生产者完成
for (Thread t : producers) {
t.join();
}
// 8. 等待消费者完成
latch.await();
long end = System.currentTimeMillis();
// 9. 关闭资源
disruptor.shutdown();
return end - start;
}
}
class LogEHandler implements EventHandler<BenchmarkConfig.LogEvent> {
private final AtomicLong counter;
private final CountDownLatch latch;
public LogEHandler(CountDownLatch latch, AtomicLong counter) {
this.counter = counter;
this.latch = latch;
}
@Override
public void onEvent(BenchmarkConfig.LogEvent event, long sequence, boolean endOfBatch) throws Exception {
long count = counter.incrementAndGet();
if (count == BenchmarkConfig.TOTAL_MESSAGES) {
latch.countDown();
}
}
}
启动测试类:
java
public class Suite {
public static void main(String[] args) throws InterruptedException {
testArrayBlockQueue();
testConcurrentLinkedQueue();
testDisruptor();
}
public static void testDisruptor() throws InterruptedException {
// 预热
System.out.println("=== Disruptor 预热中 ===");
long l = DisruptorBenchmark.runTest();
System.out.println("预热耗时 " + l);
// 正式测试
System.out.println("=== Disruptor 正式测试 ===");
long cost = DisruptorBenchmark.runTest();
printResult(cost); // 总消息数 = 80000000, 总耗时 = 6610 ms, 吞吐量 = 13333333 条/秒
}
public static void testConcurrentLinkedQueue() throws InterruptedException {
// 预热
System.out.println("=== ConcurrentLinkedQueue 预热中 ===");
long l = ConcurrentLinkedQueueBenchmark.runTest();
System.out.println("预热耗时 " + l);
// 正式测试
System.out.println("=== ConcurrentLinkedQueue 正式测试 ===");
long cost = ConcurrentLinkedQueueBenchmark.runTest();
printResult(cost); // 总消息数 = 80000000, 总耗时 = 36548 ms, 吞吐量 = 2222222 条/秒
}
public static void testArrayBlockQueue() throws InterruptedException {
// 预热
System.out.println("=== ArrayBlockingQueue 预热 ===");
long l = ArrayBlockingQueueBenchmark.runTest();
System.out.println("预热耗时 " + l);
// 正式测试
System.out.println("=== ArrayBlockingQueue 正式测试 ===");
long cost = ArrayBlockingQueueBenchmark.runTest();
printResult(cost); // 总消息数 = 80000000, 总耗时 = 21092 ms, 吞吐量 = 3809523 条/秒
}
private static void printResult(long costMs) {
System.out.println("性能测试结果: " +
"总消息数 = " + BenchmarkConfig.TOTAL_MESSAGES +
", 总耗时 = " + costMs + " ms" +
", 吞吐量 = " + (BenchmarkConfig.TOTAL_MESSAGES / (costMs / 1000)) + " 条/秒");
}
}
测试结果:
text
ArrayBlockingQueue: 总消息数 = 80000000, 总耗时 = 16661 ms, 吞吐量 = 5000000 条/秒
ConcurrentLinkedQueue: 总消息数 = 80000000, 总耗时 = 40904 ms, 吞吐量 = 2000000 条/秒
Disruptor: 总消息数 = 80000000, 总耗时 = 5845 ms, 吞吐量 = 16000000 条/秒
这里的结果是: Disruptor的性能最高,ArrayBlockingQueue其次,ConcurrentLinkedQueue反而是最低的,估计是因为高并发的情况下,竞争激烈导致CAS操作效率下降。
除此之外,还可以比较一下在不同队列大小、不同生产者数的情况下,性能如何。
原理解析
CPU缓存行的伪共享问题
CPU的三级缓存: CPU有1级缓存、2级缓存、3级缓存,1级缓存是最靠近CPU的,其它依次,越靠近CPU的缓存,速度越快,容量越小,L1、L2被单个CPU使用,L3被单个插槽上的所有CPU核共享,最后是内存。当CPU执行运算的时候,先去L1缓存取数据,如果L1没有,去L2取数据,然后依次是L3、主内存,离CPU越远,取数据的耗费时间越长。L1缓存大概256字节,L2缓存256K,L3缓存12M,不同机器上的值不一样。
缓存行: CPU三级缓存的缓存单位是缓存行,在大多数系统上,一个缓存行是64个字节,CPU每次取内存中拉取数据时,会把相邻的数据也存入缓存行,
伪共享: 多核处理器的常见性能问题。 CPU以缓存行为单位加载数据,把这些数据放到三级缓存中,当多个CPU核心频繁修改同一缓存行中的不同变量时,即使这些变量逻辑上没有关联,也会触发缓存一致性协议的协调操作。 具体而言,如果核心1修改了某一缓存行,它会向其他核心发送一条广播消息,强制它们把对应的缓存行变为无效,如果要读取该缓存行中的数据,必须从主存或者核心1中读取。
代码比较: 两个线程并发操作一个变量n次,比较没有填充的情况和有填充的情况哪个先完成?
java
// 没有缓存行填充的情况
public class CacheLineNoPadding implements SomeOneEntity {
public long x = 1L;
@Override
public void setValue(long value) {
x = value;
}
}
// 有缓存行填充的情况,这两个实体类都继承了相同的接口
public class CacheLineWithPadding implements SomeOneEntity {
// 前后各填充56个字节,加上x本身,保证x在任何情况下,都能独占一个缓存行,
protected long p1, p2, p3, p4, p5, p6, p7;
public long x = 1L;
protected long p9, p10, p11, p12, p13, p14, p15;
@Override
public void setValue(long value) {
x = value;
}
}
测试类:
java
public class SharingMain4 {
public static void main(String[] args) {
SomeOneEntity[] someOneEntities = new SomeOneEntity[2];
someOneEntities[0] = new CacheLineNoPadding();
someOneEntities[1] = new CacheLineNoPadding();
// 测试没有填充的情况
test1(someOneEntities);
SomeOneEntity[] someOneEntities2 = new SomeOneEntity[2];
someOneEntities2[0] = new CacheLineWithPadding();
someOneEntities2[1] = new CacheLineWithPadding();
// 测试没有填充的情况
test1(someOneEntities2);
}
public static void test1(SomeOneEntity[] someOneEntities) {
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread threadA = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
someOneEntities[0].setValue(i);
}
countDownLatch.countDown();
}, "threadA");
Thread threadB = new Thread(() -> {
for (int i = 100000; i < 200000; i++) {
someOneEntities[1].setValue(i);
}
countDownLatch.countDown();
}, "threadB");
long startTime = System.nanoTime();
threadA.start();
threadB.start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long endTime = System.nanoTime();
System.out.println("花费 " + (endTime - startTime) + " 纳秒");
}
}
为什么测试案例中,两个线程各自操作一个对象? 这正是伪共享的现象,当字段x没有填充时,两个对象的字段因为挨得太近,被加载到了一个缓存行,而有了填充之和,两个字段无论如何不会被加载到一个缓存行,从而消除了伪共享。
结果: 这里并发操作了20万次,没有填充的情况,花费了 10517100 纳秒,有填充的情况,花费了 2071800 纳秒,证明填充之后确实效率更高。
实战案例
log4j2使用disruptor实现异步打印日志
log4j2配置为异步打印日志后,内部使用disruptor作为队列,生产者把日志数据投递到队列中,再由消费者从队列中获取数据,完成生产者和消费者的解耦。
log4j2使用disruptor,队列大小是多少?消费者线程有几个?
- 队列大小: webapp的情况下,默认大小是 256 * 1024 个槽位,一个槽位4字节(Object类型的数组),队列占用1M内存,特殊场景下,会被调整为 4 * 1024 个槽位。 特殊场景是指没有GC的场景下。
- 消费者线程数: 1个,log4j2的瓶颈在于硬盘IO,使用单个线程进行写入的效率最高,避免多个线程竞争同一个IO资源带来的开销。