disruptor 从入门到使用

简介

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资源带来的开销。

参考

相关推荐
龙智DevSecOps解决方案1 小时前
WildFly vs JBoss vs GlassFish:2026年Java应用服务器选型对比与Perforce JRebel加速实践
java·应用服务器·jrebel·wildfly
孫治AllenSun1 小时前
【LangChain4J-04】Tool 工具的使用
java·人工智能
南城以南溫暖如初1471 小时前
树洞交友系统架构设计与匿名聊天实战指南
java·spring boot·mysql·系统架构·vue·mybatis·交友
lemon_sjdk1 小时前
JavaFX 响应式核心:从属性绑定、失效通知到生命周期管理
java
QCodingDev1 小时前
Spring AI Alibaba Graph实战:从ReAct Agent到Workflow,企业AI复杂流程该如何编排?
java·人工智能·spring·ai·ai编程·企业ai
一嘴一个橘子1 小时前
SpringDataRedis 操作 redis
java·redis
JasmineWr2 小时前
Spring BeanDefinition 与 Bean 生命周期、循环依赖解析
java·后端·spring
dzl843942 小时前
微服务基座
java
QQ_21696290962 小时前
【项目编号:project86564】SpringBoot供应链管理系统:采购、供应商、库存、销售、统计报表一体化实战
java·spring boot·后端