小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
Disruptor原理分析
Disruptor关联好任务处理事件后,就调用了disruptor.start() 方法,可以看出在调用了 start() 方法后,消费者线程就已经开启。
启动Disruptor
start() ->开启 Disruptor,运行事件处理器。
1 | java复制代码public RingBuffer<T> start(){ |
关联事件
handleEventsWith() -> createEventProcessors()调用的核心方法,作用是创建事件处理器。
1 | java复制代码@SafeVarargs |
存储事件
将EventHandler对象绑定存储到consumerRepository内部,并且交由BatchEventProcessor处理器进行代理执行。
1 | java复制代码EventHandlerGroup<T> createEventProcessors( |
- handleEventsWith() 方法中,可以看到构建了一个 BatchEventProcessor(继承了 Runnable 接口)对象,start()方法启动的同样也是这个对象的实例。
- 这个对象继承自 EventProcessor ,EventProcessor 是 Disruptor 里非常核心的一个接口,它的实现类的作用是轮询接收RingBuffer提供的事件,并在没有可处理事件是实现等待策略。
- 这个接口的实现类必须要关联一个线程去执行,通常我们不需要自己去实现它。
BatchEventProcessor类
BatchEventProcessor:主要事件循环,处理 Disruptor 中的 event,拥有消费者的 Sequence。
核心私有成员变量
- Sequence :维护当前消费者消费的 ID。
- SequenceBarrier :序号屏障,协调消费者的消费 ID,主要作用是获取消费者的可用序号,并提供等待策略的执行。
- EventHandler<? super T> :消费者的消费逻辑(我们实现的业务逻辑)。
- DataProvider :获取消费对象。RingBuffer 实现了此接口,主要是提供业务对象。
核心方法
- processEvents():由于 BatchEventProcessor 继承自 Runnable 接口,所以在前面启动它后,run() 方法会执行,而 run() 方法内部则会调用此方法。
1 | java复制代码private void processEvents() |
- 消费者事件处理器的核心代码,sequenceBarrier.waitFor(nextSequence) 方法内部,会比较当前消费者序号与可用序号的大小:
+ 当可用序号(availableSequence)大于当前消费者序号(nextSequence),再获取到当前可用的最大的事件序号 ID(waitFot()方法内部调用 sequencer.getHighestPublishedSequence(sequence, availableSequence)),进行循环消费。
+ 可用序号是维护在 ProcessingSequenceBarrier 里的,ProcessingSequenceBarrier 是通过 ringBuffer.newBarrier() 创建出来的。

由图可以看出,在获得可用序号时,SequenceBarrier 在 EventProcessor 和 RingBuffer中充当协调的角色。
多消费事件和单消费事件在dependentSequence 上的处理有一些不同,可以看下 ProcessingSequenceBarrier 的 dependentSequence 的赋值以及 get() 方法 (Util.getMinimumSequence(sequences))。
启动过程分析之生产者
首先调用了 ringBuffer.next() 方法,获取可用序号,再获取到该序号下事先通过 Eventfactory 创建好的空事件对象,在我们对空事件对象进行赋值后,再调用 publish 方法将事件发布,则消费者就可以获取进行消费了。
生产者这里的核心代码如下,这里我截取的是多生产者模式下的代码:
1 | java复制代码 public long next(int n){ |
cursor 对象和 Util.getMinimumSequence(gatingSequences, current) 方法,cursor 对象是生产者维护的一个生产者序号,标示当前生产者已经生产到哪一个位置以及下一个位置,它是 Sequence 类的一个实例化对象。
- 从图里可以看出,Sequence 继承以及间接继承了 RhsPadding 和 LhsPadding 类,而这俩个类都各定义了 7 个 long 类型的成员变量。
- 而 Sequence 的 get() 方法返回的也是一个 long 类型的值 value。这是上一篇文章介绍的充缓存行,消除伪共享。
- 在 64 位的计算机中,单个缓存行一般占 64 个字节,当 cpu 从换存里取数据时,会将该相关数据的其它数据取出来填满一个缓存行,这时如果其它数据更新,则缓存行缓存的该数据也会失效,当下次需要使用该数据时又需要重新从内存中提取数据。
- ArrayBlockingQueue 获取数据时,很容易碰到伪共享导致缓存行失效,而 Disruptor这里当在 value 的左右各填充 7 个 long 类型的数据时,每次取都能确保该数据独占缓存行,也不会有其他的数据更新导致该数据失效。避免了伪共享的问题( jdk 的并发包下也有一些消除伪共享的设计)。
RingBuffer:它是一个首尾相接的环状的容器,用来在多线程中传递数据。第一张图里面创建 Disruptor 的多个参数其实都是用来创建 RingBuffer 的,比如生产者类型(单 or 多)、实例化工厂、容器长度、等待策略等。
简单分析,多个生产者同时向 ringbuffer 投递数据,假设此时俩个生产者将 ringbuffer 已经填满,因为 sequence 的序号是自增+1(若不满足获取条件则循环挂起当前线程),所以生产的时候能保证线程安全,只需要一个 sequence 即可。
当多消费者来消费的时候,因为消费速度不同,例如消费者 1 来消费 0、1,消费者 2 消费 2、4,消费者 3 消费 3。
当消费者消费完 0 后,消费者 2 消费完 2 后,消费者 3 消费完 3 后,生产者再往队列投递数据时,其他位置还未被消费,会投递到第 0 个位置, 此时再想投递数据时,虽然消费 2 的第二个位置空缺、消费者 3 的第三个位置空缺,消费者还在消费 1 时,无法继续投递。因为是通过比较消费者自身维护的 sequence 的最小的序号,来进行比较。
Util.getMinimumSequence(gatingSequences, current) 方法也就无需再多说,它就是为了获取到多个消费者的最小序号,判断当前 ringBuffer 中的剩余可用序号是否大于消费者最小序号,是的话,则不能投递,需要阻塞当前线程(LockSupport.parkNanos(1))。
当消费者消费速度大于生产者生产者速度,生产者还未来得及往队列写入,或者生产者生产速度大于消费者消费速度,此时怎么办呢?而且上面也多次提到没有满足条件的消费事件时,消费者会等待,接下来说一下消费者的等待策略。
个人常用的策略:
- BlockingWaitStrategy 使用了锁,低效的策略。
- SleepingWaitStrategy 对生产者线程的影响最小,适合用于异步日志类似的场景。(不加锁空等)
- YieldingWaitStrategy 性能最好,适合用于低延迟的系统,在要求极高性能且之间处理线数小于 cpu 逻辑核心数的场景中,推荐使用。
1 | java复制代码@Override |
Java 8 Contended注解
- 在Java 8中,可以采用@Contended在类级别上的注释,来进行缓存行填充。这样,可以解决多线程情况下的伪共享冲突问题。
- Contended可以用于类级别的修饰,同时也可以用于字段级别的修饰,当应用于字段级别时,被注释的字段将和其他字段隔离开来,会被加载在独立的缓存行上。在字段级别上,@Contended还支持一个“contention group”属性(Class-Level不支持),同一group的字段们在内存上将是连续(64字节范围内),但和其他他字段隔离开来。
@Contended注释的行为如下所示:
在类上应用Contended:
1 | java复制代码@Contended |
将使整个字段块的两端都被填充:(以下是使用 –XX:+PrintFieldLayout的输出)
1 | sql复制代码TestContended$ContendedTest2: field layout |
注意,我们使用了128 bytes的填充 – 2倍于大多数硬件缓存行的大小(cache line一般为64 bytes) – 来避免相邻扇区预取导致的伪共享冲突。
在字段上应用Contended:
1 | java复制代码public static class ContendedTest1 { |
将导致该字段从连续的字段块中分离开来并高效的添加填充:
1 | python复制代码TestContended$ContendedTest1: field layout |
注解多个字段使他们分别被填充:
1 | java复制代码public static class ContendedTest4 { |
被注解的2个字段都被独立地填充:
1 | sql复制代码TestContended$ContendedTest4: field layout |
在有些cases中,你会想对字段进行分组,同一组的字段会和其他字段有访问冲突,但是和同一组的没有。例如,(同一个线程的)代码同时更新2个字段是很常见的情况。
1 | java复制代码public static class ContendedTest5 { |
内存布局是:
1 | sql复制代码TestContended$ContendedTest5: field layout |
@Contended在字段级别,并且带分组的情况下,是否能解决伪缓存问题。
1 | java复制代码import sun.misc.Contended; |
用2个线程来修改字段
- 测试1:线程0修改value1和value2;线程1修改value3和value4;他们都在同一组中。
- 测试2:线程0修改value1和value3;线程1修改value2和value4;他们在不同组中。
测试1
1 | java复制代码public final class FalseSharing implements Runnable { |
本文转载自: 掘金