1. Disruptor简介
- Disruptor git地址:https://github.com/LMAX-Exchange/disruptor
- Disruptor定义:线程间的高性能消息框架
- Disruptor核心思想:把多线程并发写的线程安全问题转化为线程本地写,即:不需要做同步
2. Disruptor优点
- Disruptor非常轻量,整个框架最新版3.4.2也才70多个类,但性能却非常强悍,得益于其优秀的设计和对计算机底层原理的运用
- 单线程每秒能处理超600W的数据(Disruptor能在1秒内将600W数据发送给消费者,其实600W是Disruptor刚发布时硬件的水平了,现在在个人PC上也能轻松突破2000W)
- 基于事件驱动模型,不用消费者主动拉取消息
- 比JDK的ArrayBlockingQueue性能高一个数量级
3. 为什么这么快!
- 无锁序号栅栏
- 缓存行填充,消除伪共享
- 内存预分配
- 环形队列RingBuffer
4. Disruptor核心概念
- RingBuffer:环形队列,基于数组的内存级别缓存,是创建sequencer(序号)与定义WaitStrategy(拒绝策略)的入口
- Disruptor:对RingBuffer的封装,持有RingBuffer、消费者线程池Executor、消费之集合ConsumerRepsitory等引用
- Sequence:对RingBuffer中的元素进行序号标记,通过顺序递增的方式来管理进行交换的数据(事件/Event),一个Sequence可以跟踪标识某个事件的处理进度,同时还能消除伪共享
- Sequencer:Sequencer里面包含了Sequence,是Disruptor的核心,Sequencer有两个实现类:
SingleProducerSequencer
(单生产者实现)、MultiProducerSequencer
(多生产者实现),Sequencer主要作用是实现生产者和消费者之间快速、正确传递数据的并发算法 - SequenceBarrier:消费者屏障,用于控制RingBuffer的Producer和Consumer之间的平衡关系,并且决定了Consumer是否还有可处理的事件的逻辑
- WaitStrategy:消费者等待策略,决定了消费者如何等待生产者将Event生产进Disruptor,WaitStrategy有多种实现策略,分别是:
1.BlockingWaitStrategy
:阻塞方式,效率较低,但对cpu消耗小,内部使用的是典型的锁和条件变量机制(java的ReentrantLock),来处理线程的唤醒,这个策略是disruptor等待策略中最慢的一种,但是是最保守使用消耗cpu的一种用法,并且在不同的部署环境下最能保持性能一致。 但是,随着我们可以根据部署的服务环境优化出额外的性能
2.BusySpinWaitStrategy
:自旋方式,无锁,BusySpinWaitStrategy是性能最高的等待策略,但是受部署环境的制约依赖也越强。 仅当event处理线程数少于物理核心数的时候才应该采用这种等待策略。 例如,超线程不可开启。
3.LiteBlockingWaitStrategy
:BlockingWaitStrategy的变体版本,目前感觉不建议使用
4.LiteTimeoutBlockingWaitStrategy
:LiteBlockingWaitStrategy的超时版本
5.PhasedBackoffWaitStrategy
:自旋 + yield + 自定义策略,当吞吐量和低延迟不如CPU资源重要,CPU资源紧缺,可以使用此策略
6.SleepingWaitStrategy
:自旋休眠方式(无锁),性能和BlockingWaitStrategy差不多,但是这个对生产者线程影响最小,它使用一个简单的loop繁忙等待循环,但是在循环体中间它调用了LockSupport.parkNanos(1)
。 一般情况在linux系统这样会使得线程停顿大约60微秒。不过这样做的好处是,生产者线程不需要额外的累加计数器,也不需要产生条件变量信号量开销。 负面影响是,在生产者线程与消费者线程之间传递event数据的延迟变高。所以SleepingWaitStrategy适合在不需要低延迟, 但需要很低的生产者线程影响的情形。一个典型的案例是异步日志记录功能。
7.TimeoutBlockingWaitStrategy
:BlockingWaitStrategy的超时阻塞方式
8.YieldingWaitStrategy
:自旋线程切换竞争方式(Thread.yield()
),最快的方式,适用于低延时的系统,在要求极高性能且事件处理线数小于CPU逻辑核心数的场景中推荐使用此策略,它会充分使用压榨cpu来达到降低延迟的目标。 通过不断的循环等待sequence去递增到合适的值。 在循环体内,调用Thread.yield()
来允许其他的排队线程执行。 这是一种在需要极高性能并且event handler线程数少于cpu逻辑内核数的时候推荐使用的策略。 例如,你开启了超线程。(译者注:超线程是intel研发的一种cpu技术,可以使得一个核心提供两个逻辑线程,比如4核心超线程后有8个线程)
✨✨✨✨✨这里说一下YieldingWaitStrategy使用要小心,不是特别要求性能的情况下,要谨慎使用,否则会引起服务起cpu飙升的情况,因为他的内部实现是在线程做100次递减然后Thread.yield()
,可能会压榨cpu性能来换取速度,所以如果要使用要满足上面的描述 - Evnet:从生产者到消费者过程中所处理的数据单元,Event由使用者自定义
- EventHandler:由用户自定义实现,就是我们写消费者逻辑的地方,代表了Disruptor中的一个消费者的接口
- EventProcessor:这是个事件处理器接口,实现了
Runnable
,处理主要事件循环,处理Event,拥有消费者的Sequence,这个接口有2个重要实现:
WorkProcessor
:多线程处理实现,在多生产者多消费者模式下,确保每个sequence只被一个processor消费,在同一个WorkPool中,确保多个WorkProcessor不会消费同样的sequence
BatchEventProcessor
:单线程批量处理实现,包含了Event loop有效的实现,并回调到了一个EventHandler接口的实现对象,这接口实际上是通过重写run
方法,轮询获取数据对象,并把数据经过等待策略交给消费者去处理
5. Disruptor基本使用方式
maven依赖:
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
----------------------------------------------------华丽的分割线------------------------------------------------------
5.1 Disruptor单生产单消费简单模式举例:
import lombok.Data;
/**
* 消息对象
*/
@Data
public class TestEvent {
private Long data;
}
import com.lmax.disruptor.EventFactory;
/**
* 消息对象生产工厂
*/
public class TestEventFactory implements EventFactory<TestEvent> {
@Override
public TestEvent newInstance() {
//返回空的消息对象数据Event
return new TestEvent();
}
}
import com.lmax.disruptor.EventHandler;
/**
* 消息事件处理器
*/
public class TestEventHandler implements EventHandler<TestEvent> {
/**
* 事件驱动模式
*/
@Override
public void onEvent(TestEvent event, long sequence, boolean endOfBatch) throws Exception {
// do ...
System.out.println("消费者消费处理数据:" + event.getData());
}
}
import com.lmax.disruptor.RingBuffer;
/**
* 消息发送
*/
public class TestEventProducer {
private RingBuffer<TestEvent> ringBuffer;
public TestEventProducer(RingBuffer<TestEvent> ringBuffer) {
this.ringBuffer = ringBuffer;
}
/**
* 生产的数据发送出去
* @param data
*/
public void sendData(long data){
//从ringBuffer获取可用sequence序号
long sequence = ringBuffer.next();
try {
//根据sequence获取sequence对应的Event 这个Event是一个没有赋值具体数据的对象
TestEvent testEvent = ringBuffer.get(sequence);
testEvent.setData(data);
} finally {
//提交发布
ringBuffer.publish(sequence);
}
}
}
import com.lmax.disruptor.BlockingWaitStrategy;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 测试类:
*/
public class TestMain {
public static void main(String[] args) {
TestEventFactory eventFactory = new TestEventFactory();
int ringBufferSize = 1024 * 1024;
//这个线程池最好自定义
ExecutorService executor = Executors.newCachedThreadPool();
//实例化disruptor
Disruptor<TestEvent> disruptor = new Disruptor<TestEvent>(
eventFactory, //消息工厂
ringBufferSize, //ringBuffer容器最大容量长度
executor, //线程池,最好自定义一个
ProducerType.SINGLE, //单生产者模式
new BlockingWaitStrategy() //等待策略
);
//添加消费者监听 把TestEventHandler绑定到disruptor
disruptor.handleEventsWith(new TestEventHandler());
//启动disruptor
disruptor.start();
//获取实际存储数据的容器RingBuffer
RingBuffer<TestEvent> ringBuffer = disruptor.getRingBuffer();
//生产发送数据
TestEventProducer producer = new TestEventProducer(ringBuffer);
for(long i = 0; i < 100; i ++){
producer.sendData(i);
}
disruptor.shutdown();
executor.shutdown();
}
}
运行结果:
消费者消费处理数据:0
消费者消费处理数据:1
消费者消费处理数据:2
......
消费者消费处理数据:97
消费者消费处理数据:98
消费者消费处理数据:99
上面这写代码展示了单消费者单生产者的简单处理逻辑
----------------------------------------------------华丽的分割线------------------------------------------------------
5.2 Disruptor单生产单消费复杂模型举例:
import lombok.Data;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Event 数据
*/
@Data
public class Test1 {
private String name;
private Long id;
private Integer num;
private Boolean flag = true;
}
import com.lmax.disruptor.EventHandler;
//event处理类
public class Test1HandlerPrint implements EventHandler<Test1> {
@Override
public void onEvent(Test1 event, long sequence, boolean endOfBatch) throws Exception {
System.out.println(event);
}
}
import com.lmax.disruptor.EventHandler;
//event处理类
public class Test1HandlerSetFlag implements EventHandler<Test1> {
@Override
public void onEvent(Test1 event, long sequence, boolean endOfBatch) throws Exception {
System.out.println("event set Flag false");
event.setFlag(false);
}
}
import com.lmax.disruptor.EventHandler;
//event处理类
public class Test1HandlerSetId implements EventHandler<Test1> {
@Override
public void onEvent(Test1 event, long sequence, boolean endOfBatch) throws Exception {
System.out.println("event set id 12345");
event.setId(12345L);
}
}
import com.lmax.disruptor.EventHandler;
//event处理类
public class Test1HandlerSetName implements EventHandler<Test1> {
@Override
public void onEvent(Test1 event, long sequence, boolean endOfBatch) throws Exception {
System.out.println("event set name Test1");
event.setName("Test1");
}
}
import com.lmax.disruptor.EventTranslator;
import com.lmax.disruptor.dsl.Disruptor;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
//事件/消息 投递处理类
public class Test1Pushlisher implements Runnable {
private Disruptor<Test1> disruptor;
private CountDownLatch downLatch;
public Test1Pushlisher(Disruptor<Test1> disruptor, CountDownLatch downLatch) {
this.disruptor = disruptor;
this.downLatch = downLatch;
}
@Override
public void run() {
Test1EventTranslator test1EventTranslator = new Test1EventTranslator();
//提交要处理的任务
disruptor.publishEvent(test1EventTranslator);
downLatch.countDown();
}
}
class Test1EventTranslator implements EventTranslator<Test1>{
//快速/便捷 的提交要处理的任务 不用再获取ringBuffer和可用sequence序号
//在对象被创建的时候会自动执行此方法 处理Event
@Override
public void translateTo(Test1 event, long sequence) {
event.setNum(new Random().nextInt());
}
}
import com.lmax.disruptor.BusySpinWaitStrategy;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test1Main {
public static void main(String[] args) throws InterruptedException {
int ringBufferSize = 1024 * 1024;
//这个线程池最好自定义
ExecutorService executor = Executors.newCachedThreadPool();
Disruptor<Test1> disruptor = new Disruptor(
(EventFactory<Test1>) () -> new Test1(), //简写工厂创建Event
ringBufferSize,
//disruptor使用的线程池
// 这个线程池在单消费者模式下
// 有几个消费者Handler就最少需要初始化几个线程
// 多消费者就不会有这个问题
executor,
ProducerType.SINGLE,
new BusySpinWaitStrategy());
//绑定Event处理类和disruptor,并设置执行顺序
// 通过链式编程的方式决定串行,通过多参数的方式决定并行,看下面的例子
// 下面这个写法有2种执行结果:
/**
* 1:
* event set id 12345
* Test1(name=null, id=12345, num=1463263591, count=0)
* event set name Test1
* Test1(name=Test1, id=12345, num=1463263591, count=0)
*
* 2:
* Test1(name=null, id=null, num=-288133781, count=0)
* event set id 12345
* event set name Test1
* Test1(name=Test1, id=12345, num=-288133781, count=0)
*
* 可以多执行几次下面这个写法,可以看出来,他是Test1HandlerSetId和Test1HandlerPrint并行执行
* 然后等Test1HandlerSetId和Test1HandlerPrint并行执行完成之后
* 再串行执行Test1HandlerSetName和第二次的Test1HandlerPrint
* 这就是disruptor.handleEventsWith的串并行操作
**/
// disruptor.handleEventsWith(new Test1HandlerSetId(), new Test1HandlerPrint())
// .handleEventsWith(new Test1HandlerSetName())
// .handleEventsWith(new Test1HandlerPrint());
//下面这个写法就是完全的串行操作
/**
* 执行顺序是:Test1HandlerPrint -> Test1HandlerSetName -> Test1HandlerPrint -> Test1HandlerSetId -> Test1HandlerPrint
* 执行结果:
* Test1(name=null, id=null, num=1780818646, count=0)
* event set name Test1
* Test1(name=Test1, id=null, num=1780818646, count=0)
* event set id 12345
* Test1(name=Test1, id=12345, num=1780818646, count=0)
*/
// disruptor.handleEventsWith(new Test1HandlerPrint())
// .handleEventsWith(new Test1HandlerSetName())
// .handleEventsWith(new Test1HandlerPrint())
// .handleEventsWith(new Test1HandlerSetId())
// .handleEventsWith(new Test1HandlerPrint());
/**
* 通过上面两个例子,应该可以想到,disruptor支持串并行操作,类似多边形操作,多线并线
* 还有其他的写法,比如并行可以写:
* disruptor.handleEventsWith(new Test1HandlerPrint());
* disruptor.handleEventsWith(new Test1HandlerSetName());
* 这样就是Test1HandlerPrint和Test1HandlerSetName并行执行
* 也可以使用disruptor.handleEventsWith的返回结果:EventHandlerGroup
* EventHandlerGroup有then()、and()等方法,也可以实现复杂等串并行操作
*/
//下面演示一个更复杂等操作,多边形操作:
Test1HandlerPrint print1 = new Test1HandlerPrint();
Test1HandlerPrint print2 = new Test1HandlerPrint();
Test1HandlerPrint print3 = new Test1HandlerPrint();
Test1HandlerPrint print4 = new Test1HandlerPrint();
Test1HandlerSetName setName = new Test1HandlerSetName();
Test1HandlerSetId setId = new Test1HandlerSetId();
Test1HandlerSetFlag setFlag = new Test1HandlerSetFlag();
disruptor.handleEventsWith(print1);
disruptor.after(print1).handleEventsWith(setId, setName);
disruptor.after(setId).handleEventsWith(print2);
disruptor.after(setName).handleEventsWith(print3);
disruptor.after(print2, print3).handleEventsWith(setFlag);
disruptor.after(setFlag).handleEventsWith(print4);
/**
* 上面这个操作的顺序是:
* ↗ setId -> print1 ↘
* print3 setFlag -> print4
* ↘ setName -> setFlag ↗
*/
//启动
disruptor.start();
CountDownLatch downLatch = new CountDownLatch(1);
//线程池投递任务
executor.submit(new Test1Pushlisher(disruptor, downLatch));
downLatch.await();
disruptor.shutdown();
executor.shutdown();
System.out.println("over");
}
}
上面这些代码都有注释,大家可以拷下来执行看看,主要是想给大家展示单生产单消费的复杂模型,也就是复杂消费链路的写法
------------------------------------------------华丽的分割线--------------------------------------------------------------
5.3 Disruptor多生产者多消费举例:
import lombok.Data;
@Data
public class Item {
private String name;
private Long id;
private Integer num;
}
import com.lmax.disruptor.ExceptionHandler;
public class EventExceptionHander implements ExceptionHandler<Item> {
@Override
public void handleEventException(Throwable ex, long sequence, Item event) {
System.out.println("消费时的异常");
}
@Override
public void handleOnStartException(Throwable ex) {
System.out.println("消费刚启动时的异常");
}
@Override
public void handleOnShutdownException(Throwable ex) {
System.out.println("Shutdown时候的异常");
}
}
import com.lmax.disruptor.RingBuffer;
public class Producer {
private RingBuffer<Item> ringBuffer;
public Producer(RingBuffer<Item> ringBuffer) {
this.ringBuffer = ringBuffer;
}
public void sendData(String data) {
//获取下一个可用的sequence
long sequence = ringBuffer.next();
//设置并投递event数据
try {
Item item = ringBuffer.get(sequence);
item.setName(data);
} finally {
ringBuffer.publish(sequence);
}
}
}
import com.lmax.disruptor.WorkHandler;
import java.util.concurrent.atomic.AtomicInteger;
public class TestConsumer implements WorkHandler<Item> {
private String consumerId;
private AtomicInteger atomicInteger;
public TestConsumer(String consumerId, AtomicInteger atomicInteger) {
this.consumerId = consumerId;
this.atomicInteger = atomicInteger;
}
@Override
public void onEvent(Item event) throws Exception {
Thread.sleep(5L);
System.out.println("消费者:" + consumerId + "消费Item:" + event);
atomicInteger.incrementAndGet();
}
}
import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.ProducerType;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class TestMain {
public static void main(String[] args) throws InterruptedException {
//构建一个RingBuffer
RingBuffer<Item> ringBuffer = RingBuffer.create(
ProducerType.MULTI,
() -> new Item(),
1024 * 1024,
new YieldingWaitStrategy()
);
//通过RingBuffer创建一个屏障
SequenceBarrier sequenceBarrier = ringBuffer.newBarrier();
//创建多个消费者的数组 和 消费统计atomicInteger
AtomicInteger atomicInteger = new AtomicInteger(0);
TestConsumer[] testConsumers = new TestConsumer[10];
for(int i = 0; i < testConsumers.length; i++){
testConsumers[i] = new TestConsumer("consumers" + i, atomicInteger);
}
//构建多消费者的工作池
WorkerPool workerPool = new WorkerPool(
ringBuffer,
sequenceBarrier,
new EventExceptionHander(),
testConsumers
);
//设置多消费者的sequence序号,用于单独统计消费进度
ringBuffer.addGatingSequences(workerPool.getWorkerSequences());
//启动workerPool
workerPool.start(Executors.newCachedThreadPool());
CountDownLatch latch = new CountDownLatch(1);
//等待所有生产者投递完成所有数据 下面是100个线程每个线程投递100次 所以总数10000
CountDownLatch latch1 = new CountDownLatch(10000);
//循环模拟多线程生产数据
for(int i = 0; i < 100; i++){
Producer producer = new Producer(ringBuffer);
new Thread(() -> {
try {
//构建100个线程并在这里等待
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j = 0; j < 100; j++){
producer.sendData("data:" + j);
latch1.countDown();
}
}).start();
}
//sleep等待100线程创建完成
Thread.sleep(2000L);
System.out.println("所有线程都构建完成之后通知他们可以开始生产数据");
latch.countDown();
latch1.await();
System.out.println("所有生产者已完成数据发送");
//sleep模拟消费数据
Thread.sleep(10000L);
System.out.println("当前消费数据总数:" + atomicInteger.get());
}
}
运行结果:
所有线程都构建完成之后通知他们可以开始生产数据
所有生产者已完成数据发送
消费者:consumers7消费Item:Item(name=data:2, id=null, num=null)
消费者:consumers9消费Item:Item(name=data:3, id=null, num=null)
消费者:consumers0消费Item:Item(name=data:0, id=null, num=null)
消费者:consumers3消费Item:Item(name=data:0, id=null, num=null)
消费者:consumers8消费Item:Item(name=data:2, id=null, num=null)
...........
消费者:consumers7消费Item:Item(name=data:95, id=null, num=null)
消费者:consumers1消费Item:Item(name=data:98, id=null, num=null)
消费者:consumers2消费Item:Item(name=data:99, id=null, num=null)
消费者:consumers0消费Item:Item(name=data:94, id=null, num=null)
当前消费数据总数:10000
上面这个运行结果 输出:当前消费数据总数:10000
是在Thread.sleep(10000L);
模拟消费完成之后输出,电脑性能不行的话可能并不一定输出在最后面,如果不在最后面建议sleep时间加长一点
------------------------------------------------华丽的分割线--------------------------------------------------------------
上面列举了3种Disruptor的使用简单举例,实际项目使用要灵活应用
6. Disruptor原理分析
6.1 缓存行填充 消除伪共享源码解析
这里以RingBuffer中的消除伪共享为例
要了解缓存行填充消除伪共享,首先要了解什么是系统缓存行:
CPU 为了更快的执行代码。于是当从内存中读取数据时,并不是只读自己想要的部分。而是读取足够的字节来填入高速缓存行。根据不同的 CPU ,高速缓存行大小不同。如 X86 是 32BYTES ,而 ALPHA 是 64BYTES 。并且始终在第 32 个字节或第 64 个字节处对齐。这样,当 CPU 访问相邻的数据时,就不必每次都从内存中读取,提高了速度。 因为访问内存要比访问高速缓存用的时间多得多。
这个缓存是CPU内部自己的缓存,内部的缓存单位是行,叫做缓存行。在多核环境下会出现CPU之间的内存同步问题(比如一个核加载了一份缓存,另外一个核也要用到同一份数据),如果每个核每次需要时都往内存中存取(一个在读缓存,一个在写缓存时,造成数据不一致),这会带来比较大的性能损耗。
现在需要注意一件有趣的事情,数据在缓存中不是以独立的项来存储的,如不是一个单独的变量,也不是一个单独的指针。缓存是由缓存行组成的,通常是64字节(译注:这篇文章发表时常用处理器的缓存行是64字节的,比较旧的处理器缓存行是32字节),并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。
非常奇妙的是如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。我在第一篇关于ring buffer的文章中顺便提到过这个,它解释了我们的ring buffer使用数组的原因。
因此如果你数据结构中的项在内存中不是彼此相邻的(链表,我正在关注你呢),你将得不到免费缓存加载所带来的优势。并且在这些数据结构中的每一个项都可能会出现缓存未命中。
不过,所有这种免费加载有一个弊端。设想你的long类型的数据不是数组的一部分。设想它只是一个单独的变量。让我们称它为head,这么称呼它其实没有什么原因。然后再设想在你的类中有另一个变量紧挨着它。让我们直接称它为tail。现在,当你加载head到缓存的时候,你也免费加载了tail
摘自:并发编程网->神奇的缓存行填充
从CPU到 | 大约需要的 CPU 周期 | 大约需要的时间 |
---|---|---|
主存 | 约60-80纳秒 | |
QPI 总线传输(between sockets, not drawn) | 约20ns | |
L3 cache | 约40-45 周期 | 约15ns |
L2 cache | 约10 周期 | 约3ns |
L1 cache | 约3-4 周期 | 约1ns |
寄存器 | 1周期 |
简单理解就是在系统缓存中是以缓存行为单位缓存数据的,这个时候如果你要获取某个缓存数据,你会附带的获取了其他的(和你目标数据在同一个缓存行的数据),这个感觉是一个好事情,但是在多线程中,你的缓存行中存储的多个数据,有可能你想读取其中一个,但是这个时候被另一个线程修改了这个缓存行中的另一个,导致整个缓存行缓存的数据失效,整个缓存行需要从主内存重新读取,这个时候就会出现缓存未命中,拖慢了速度
那么接下来以RingBuffer为例看看他是怎么处理的
在上面第四节里面我们将disruptor核心概念中说了Sequence:对RingBuffer中的元素进行序号标记,通过顺序递增的方式来管理进行交换的数据(事件/Event),一个Sequence可以跟踪标识某个事件的处理进度,同时还能消除伪共享
这个Sequence就是:com.lmax.disruptor.Sequence
class LhsPadding
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
class Value extends LhsPadding
{
protected volatile long value;
}
class RhsPadding extends Value
{
protected long p9, p10, p11, p12, p13, p14, p15;
}
public class Sequence extends RhsPadding
{
......
}
这是Sequence(序号)的源码,这里省略了其他的代码,我们可以看到
Sequence ➡️ RhsPadding ➡️ Value ➡️ LhsPadding
这个里面的Value
就是Sequence的实际值,是个volatile long
,RhsPadding
便是右边的填充,LhsPadding
表示左边的填充,左右两边各填充7个long,这样在缓存行中,无论Sequence的value在缓存行什么位置,都可以保证不会有别的缓存影响到它自己(自己独占一个缓存行,空间换时间),这就是缓存行填充,消除伪共享
这是disruptor中RingBuffer的做法,其实在java中也支持消除伪共享,在java8之后,java提供了一个注解:@Contended
,这个注解在ConcurrentHashMap
中有使用,但是这个注解要使用执行时,必须加上虚拟机参数-XX:-RestrictContended
,@Contended注释才会生效。
6.2 内存预分配
内存预分配指的是Disruptor在初始化的时候,把ringBuffer中的所有内存都提前加载,省去了在消除投递和处理过程中的创建和分配内存,具体代码可以看下面的流程:
com.lmax.disruptor.dsl.Disruptor#Disruptor(com.lmax.disruptor.EventFactory<T>, int, java.util.concurrent.Executor, com.lmax.disruptor.dsl.ProducerType, com.lmax.disruptor.WaitStrategy)
⬇️
com.lmax.disruptor.RingBuffer#create
⬇️
com.lmax.disruptor.RingBuffer#createSingleProducer(com.lmax.disruptor.EventFactory<E>, int, com.lmax.disruptor.WaitStrategy)
//这里以单消费者为例
⬇️
com.lmax.disruptor.RingBuffer#RingBuffer
⬇️
com.lmax.disruptor.RingBufferFields#RingBufferFields
⬇️
com.lmax.disruptor.RingBufferFields#fill
按照上面这个流程,从创建Disruptor的时候,最终在com.lmax.disruptor.RingBufferFields#fill
方法里面可以看到在初始化的时候内存就被预先创建好了:
private void fill(EventFactory<E> eventFactory)
{
//创建bufferSize次循环
for (int i = 0; i < bufferSize; i++)
{
//给RingBuffer里面按照下表预先分配内存
entries[BUFFER_PAD + i] = eventFactory.newInstance();
}
}
6.3 序号栅栏机制
这里以生产这投递数据到RingBuffer开始切入,在上面单生产者单消费者简单模式举例中,有这样一段代码:
/**
* 生产的数据发送出去
* @param data
*/
public void sendData(long data){
//从ringBuffer获取可用sequence序号
long sequence = ringBuffer.next();
try {
//根据sequence获取sequence对应的Event 这个Event是一个没有赋值具体数据的对象
TestEvent testEvent = ringBuffer.get(sequence);
testEvent.setData(data);
} finally {
//提交发布
ringBuffer.publish(sequence);
}
}
在这段代码中的ringBuffer.next();
是获取下一个可用序号,用来给这个序号投递数据,点进源码之后,在单生产者中可以看到具体实现在:com.lmax.disruptor.SingleProducerSequencer#next()
@Override
public long next()
{
return next(1);
}
再看next(1);
下面这段代码就是具体的序号栅栏机制实现
@Override
public long next(int n)
{
//校验参数 Sequence初始值为-1 所以 n必须不小于1 (-1 + 1 = 0)保证下标可用
if (n < 1)
{
throw new IllegalArgumentException("n must be > 0");
}
//在初始化的时候this.nextValue值为:-1 表示下一个可用序号
long nextValue = this.nextValue;
//当在初始化后第一次调用当前方法时,那么nextValue=-1, n=1
//那么nextSequence=0 也就是环形队列的第一个
//在后续的调用中也表示下一个序号
long nextSequence = nextValue + n;
//当nextSequence=0,bufferSize为环形队列长度n,那么wrapPoint=0-n, wrapPoint就是-n 这是一个巧妙的算法逻辑 需要大家仔细想清楚
//这里先解释出来wrapPoint,它是用来判断生产者的序号减去RingBuffer环的长度,计算的结果用来判断生产者的序号有没有追上消费者最小的消费序号
long wrapPoint = nextSequence - bufferSize;
//cachedValue表示缓存的序号值,初始值也是-1
long cachedGatingSequence = this.cachedValue;
//这个if判断逻辑建议先跳过 看if里面的逻辑,再回过头来看if判断
//在理解了下面这个if里面的逻辑之后,就可以发现cachedGatingSequence就是最小的消费者序号
//如果wrapPoint大于了最小的消费者序号或者最小消费者序号大于了naxtValue
//那说明需要检查是不是需要自旋挂起 这个if判断在一定程度上减少了获取最小消费者序号的步骤,提高了性能
//至于这个为什么用wrapPoint来判断,大家可以用断点的方式看看,其实是一个算法的逻辑
//🌟🌟🌟🌟🌟(这里很重要)建议大家用断点的方式,设置4个RingBuffer长度,就是bufferSize=4,然后连续给RingBuffer投递5个数据,在消费者消费第一个数据的时候让Sleep一个很长的时间或者在消费的时候断点卡住,也就是不要让第一个消费完,模拟生产者速度大于消费者速度的情况,当投递第5个数据的时候就可以看到栅栏的作用了,也就是这个里的算法逻辑
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
{
//Volatile方式设置读取或者写入的屏障 隔离之前的调用和下次调用
cursor.setVolatile(nextValue); // StoreLoad fence
//定义一个最小的序号
long minSequence;
//自旋(相当于等待/自旋锁 避免CAS或者加锁),
//如果wrapPoint大于当前所有消费者中最小的序号值 那么就一直自旋挂起当前线程1纳秒
//因为RingBuffer是环形的,生产者生产的时候不能把消费者未消费的数据覆盖掉,也就是生产者生产太快了 需要挂起等待数据被消费
//Util.getMinimumSequence(gatingSequences, nextValue)就表示获取所有消费者中最小的Sequence值
//gatingSequences其实是在disruptor.handleEventsWith(...)里面添加的消费者Sequence 可以在com.lmax.disruptor.dsl.Disruptor#updateGatingSequencesForNextInChain方法看到:ringBuffer.addGatingSequences(processorSequences);
//这个while里面的判断条件实际意义是:生产者序号如果要大于所有消费者中的最小的序号 那么挂起自旋,保证生产的消息不会把为消费的消息覆盖掉
while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
{
//线程挂起1纳秒
LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
}
//当上面自旋结束之后,把最小的消费者序号赋值给cachedValue做记录
//cachedValue会再下次进入方法后赋值给上面的cachedGatingSequence变量
//这样再下次进入的时候不用再次在Util.getMinimumSequence(gatingSequences, nextValue)获取消费者最小序号值,直接使用缓存值提高性能
this.cachedValue = minSequence;
}
//通过上面的判断或者自旋结束,说明有可用的序号了,那么把下一个可用的序号返回
this.nextValue = nextSequence;
return nextSequence;
}
上面的代码和注释,详细的解释了序号栅栏的方式阻塞生产者生产速度大于消费者消费速度时的情况,相比传统的加锁和CAS方式,这种处理方式有高效速度快的优点
6.4 EventProcessor消费机制
在我们上面的代码举例中,有一段这样的代码:
/**
* 消息事件处理器
*/
public class TestEventHandler implements EventHandler<TestEvent> {
/**
* 事件驱动模式
*/
@Override
public void onEvent(TestEvent event, long sequence, boolean endOfBatch) throws Exception {
// do ...
System.out.println("消费者消费处理数据:" + event.getData());
}
}
他是个事件驱动类型的,不需要我们手动去拉取消息消费,点进onEvent()
方法的上游可以看到他是接口:com.lmax.disruptor.EventHandler#onEvent
下的,我们查找一下这个onEvent()
在哪里被调用过,就会发现是在:com.lmax.disruptor.BatchEventProcessor#processEvents
被调用了,很明显可以看到这是消费的逻辑,那么com.lmax.disruptor.BatchEventProcessor#processEvents
被调用是在com.lmax.disruptor.BatchEventProcessor#run
,很明显这个是Runnable
的run()
方法
这里我们重点来看com.lmax.disruptor.BatchEventProcessor#processEvents
这个方法:
private void processEvents()
{
//定义一个消息对象,实际就是我们使用时的Event消息对象
T event = null;
//下一个序号,是给消费者使用,用于获取下一个要消费的消息
long nextSequence = sequence.get() + 1L;
while (true)
{
try
{
//availableSequence表示真实可用的序号
//sequenceBarrier.waitFor 实际是从我们的阻塞策略里获取下一个可用序号 实际要看我们使用的是哪个阻塞策略
//这里真实获取的availableSequence不一定就是nextSequence 因为在多生产者(多线程)情况下,下一个可用的Sequence不一定是nextSequence
final long availableSequence = sequenceBarrier.waitFor(nextSequence);
if (batchStartAware != null)
{
batchStartAware.onBatchStart(availableSequence - nextSequence + 1);
}
//所以这里进行判断,如果nextSequence<=availableSequence 说明获取的真实可用的序号大于等于了上面sequence.get()获取的下一个序号,那么就要循环
while (nextSequence <= availableSequence)
{
//把sequence.get()的nextSequence到真实可用的序号availableSequence 之间的消息取出来
event = dataProvider.get(nextSequence);
//然后调用onEvent方法,其实就是我们上面的TestEventHandler消费者实现的onEvent
eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
//这里消费完一个自增一下用于循环
nextSequence++;
}
// 设置下一个要消费的sequence
sequence.set(availableSequence);
}
catch (final TimeoutException e)
{
//while (true)循环退出的条件,这里判断程序抛出:TimeoutException时退出
notifyTimeout(sequence.get());
}
catch (final AlertException ex)
{
//while (true)循环退出的条件,这里判断程序阻断的时候break
if (running.get() != RUNNING)
{
break;
}
}
catch (final Throwable ex)
{
//其他异常的时候 进入我们的补偿机制,就是上面例子中我们定义的异常处理情况:EventExceptionHander 可以看上面:5.3 Disruptor多生产者多消费举例
exceptionHandler.handleEventException(ex, nextSequence, event);
sequence.set(nextSequence);
nextSequence++;
}
}
}
7.总结
Disruptor作为一个以高性能著称的队列,它既实现了例如BlockingQueue等队列的功能,同时也通过缓存行填充、序号栅栏、内存预分配等机制有效等提高了性能,同时还有其他队列所没有的功能,例如:同一个消息可以有多个消费者去消费,消费者消费时既可以并行处理消费,也可以相互依赖形成并行串行同时存在的优先级消费(通过代码编程的方式,形成处理流程)
以上只是对Disruptor的部分代码解析和使用,更多的使用和源码理解,需要大家再仔细看哈,有不对的地方希望大家可以帮我指正。