本文旨在介绍Disruptor3.0的实现细节,首先从整体上描述了Disruptor3.0的核心类图,Disruptor3.0 DSL(领域专用语言)的实现类图,并以Disruptor官方列举的几大特性作为行文思路,看看Disruptor3.0是如何实现这些特性的:内存预加载、消除‘伪共享’、序号栅栏和序号配合使用来消除锁和CAS、批处理效应的具体实现等。

核心类图

  • RingBuffer——Disruptor底层数据结构实现,核心类,是线程间交换数据的中转地;
  • Sequencer——序号管理器,负责消费者/生产者各自序号、序号栅栏的管理和协调;
  • Sequence——序号,声明一个序号,用于跟踪ringbuffer中任务的变化和消费者的消费情况;
  • SequenceBarrier——序号栅栏,管理和协调生产者的游标序号和各个消费者的序号,确保生产者不会覆盖消费者未来得及处理的消息,确保存在依赖的消费者之间能够按照正确的顺序处理;
  • EventProcessor——事件处理器,监听RingBuffer的事件,并消费可用事件,从RingBuffer读取的事件会交由实际的生产者实现类来消费;它会一直侦听下一个可用的序号,直到该序号对应的事件已经准备好。
  • EventHandler——业务处理器,是实际消费者的接口,完成具体的业务逻辑实现,第三方实现该接口;代表着消费者。
  • Producer——生产者接口,第三方线程充当该角色,producer向RingBuffer写入事件。

DSL类图

以下是Disruptor3.0 DSL(domain specific language 特定领域语言)的类图,可以大致知道第三方如何继承Disruptor3.0实现具体业务逻辑。

  • Disruptor——对外暴露的门面类,提供start(),stop(),消费者事件注册,生产者事件发布等api;
  • RingBuffer——对生产者提供下一序号获取、entry元素获取、entry数据更改等api;
  • EventHandler——消费者的接口定义,提供onEvent()方法,负责具体业务逻辑实现;
  • EventHandlerGroup——业务处理器分组,管理多个业务处理器的依赖关系,提供then()、before()、after()等api。

  以下给出代码demo阐述第三方如何简单继承Disruptor3.0:

    public static void main(String[] args) throws Exception
{
// The ThreadFactory for create producer thread.
ThreadFactory producerFactory = new ProducerFactory(); // The factory for the event
LongEventFactory eventFactory = new LongEventFactory(); // Specify the size of the ring buffer, must be power of 2.
int bufferSize = 8; // Construct the Disruptor,创建Disruptor组件
Disruptor<LongEvent> disruptor = new Disruptor<>(
eventFactory,
bufferSize,
producerFactory,
ProducerType.SINGLE,
new BlockingWaitStrategy()
); // Connect the handler,绑定消费者事件,可以是多个
disruptor.handleEventsWith(new LongEventHandler());
disruptor.handleEventsWith(new LogEventHandler()); // Start the Disruptor, starts all threads running,启动Disruptor,启动所有线程,主要是消费者对应的EventProcessor侦听线程,消费者事件处理器开始侦听RingBuffer中的消息
disruptor.start(); // Get the ring buffer from the Disruptor to be used for publishing.
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer(); LongEventProducer producer = new LongEventProducer(ringBuffer); ByteBuffer bb = ByteBuffer.allocate(8);
for (long l = 0; true; l++)
{
bb.putLong(0, l);
       //生产者向RingBuffer中写入消息
producer.onData(bb);
Thread.sleep(10);
}
}

关键时序图

下图展示了Disruptor3.0整个运行过程的时序图,包括:始化、启动、处理过程。

内存预分配

RingBuffer使用数组Object[] entries作为存储元素,如下图所示,初始化RingBuffer时,会将所有的entries的每个元素指定为特定的Event,这时候event中的detail属性是null;后面生产者向RingBuffer中写入消息时,RingBuffer不是直接将enties[7]指向其他的event对象,而是先获取event对象,然后更改event对象的detail属性;消费者在消费时,也是从RingBuffer中读取出event,然后取出其detail属性。可以看出,生产/消费过程中,RingBuffer的entities[7]元素并未发生任何变化,未产生临时对象,entities及其元素对象一直存活,知道RingBuffer消亡。故而可以最小化GC的频率,提升性能。

注:图中对象Entry写错,应当为Event。

以下是RingBuffer.java类中初始化enties数组的源码:

    private void fill(EventFactory<E> eventFactory)
{
for (int i = 0; i < bufferSize; i++)
{
entries[BUFFER_PAD + i] = eventFactory.newInstance(); //使用工厂方法初始化enties元素
}
}

消费者写入数据到entry中:

//消费者实现EventHandler接口
public class LongEventHandler implements EventHandler<LongEvent>
{
  //event为从RingBuffer entry中读取的事件内容,消费者从event中读取数据,并完成业务逻辑处理
public void onEvent(LongEvent event, long sequence, boolean endOfBatch)
{
System.out.println(Thread.currentThread().getName() + " say : process LONG Event: " + event);
}
}

生产者从entry中读取数据:

public class LongEventProducer
{
  //生产者持有RingBuffer实例,可以直接向RingBuffer实例中的entry写入数据
private final RingBuffer<LongEvent> ringBuffer; public LongEventProducer(RingBuffer<LongEvent> ringBuffer)
{
this.ringBuffer = ringBuffer;
} public void onData(ByteBuffer bb)
{
long sequence = ringBuffer.next(); // Grab the next sequence
try
{
       //从ringBuffer实例中获取entry
LongEvent event = ringBuffer.get(sequence); // Get the entry in the Disruptor
// for the sequence
       //生产者将数据写入entry
event.set(bb.getLong(0)); // Fill with data
}
finally
{
       //生产者向ringBuffer提交数据变更
ringBuffer.publish(sequence);
}
}
}

可以看出:生产者未更改ringBuffer实例中entry对象,只是更改了entry中的数据,避免了过多创建临时entry对象带来的GC,进而降低了性能损耗。

消除‘伪共享’

如果两个不同的并发变量位于同一个缓存行,则在并发情况下,会互相影响到彼此的缓存有效性,进而影响到性能,这叫着‘伪共享’。为了避开‘伪共享’,Disruptor3.0在Sequence.java中使用多个long变量填充,从而确保一个序号独占一个缓存行。关于缓存行和‘伪共享’请参考:伪共享(False Sharing)

具体实现代码如下:

//在序号实际value变量(long型)左边填充7个long变量
class LhsPadding
{
protected long p1, p2, p3, p4, p5, p6, p7;
} class Value extends LhsPadding
{
protected volatile long value;
}
//在序号实际value变量(long型)右边填充7个long变量
class RhsPadding extends Value {
  protected long p9, p10, p11, p12, p13, p14, p15;
} public class Sequence extends RhsPadding {
  static final long INITIAL_VALUE = -1L;
  public Sequence() {
    this(INITIAL_VALUE);
  }
  ......
}
Sequence实际value变量的左右均被填充了7个long型变量,其自身也是long型变量,一个long型变量占据8个字节,所以序号与他上一个/下一个序号之间的最小内存分布距离为:7*8=56byte,加上自身的8个byte,可以确保序号变量独占长度为64byte(通常的一个缓存行长度)缓存行。

序号栅栏和序号配合使用来消除锁和CAS

Disruptor3.0中,序号栅栏(SequenceBarrier)和序号(Sequence)搭配使用,协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用。在Disruptor3.0中,各个消费者和生产者持有自己的序号,这些序号的变化必须满足如下基本条件:

  • 消费者序号数值必须小于生产者序号数值;
  • 消费者序号数值必须小于其前置(依赖关系)消费者的序号数值;
  • 生产者序号数值不能大于消费者中最小的序号数值,以避免生产者速度过快,将还未来得及消费的消息覆盖。

上述前两点是在SequenceBarrier的waitFor()方法中完成的,源码如下:

   public long waitFor(final long sequence) //sequence参数是该消费者期望获取的下一个序号值
throws AlertException, InterruptedException, TimeoutException
{
checkAlert();
     //根据配置的waitStrategy策略,等待期望的下一序号值变得可用
     //这里并不保证返回值availableSequence一定等于 given sequence,他们的大小关系取决于采用的WaitStrategy。
     //eg. 1、YieldingWaitStrategy在自旋100次尝试后,会直接返回dependentSequence的最小seq,这时并不保证返回值>=given sequence
     // 2、BlockingWaitStrategy则会阻塞等待given sequence可用为止,可用并不是说availableSequence == given sequence,而应当是指 >=
long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);
//如果当前可用的序号小于期望获取的下一个序号,则返回availableSequence,这将导致调用者EventProcessor继续wait
if (availableSequence < sequence)
{
return availableSequence;
}
     //这一句是‘批处理’的精妙所在,放在后面讲
return sequencer.getHighestPublishedSequence(sequence, availableSequence);
}

上面第三点是针对生产者建立的Barrier,逻辑判定发生在生产者从ringBuffer获取下一个可用的entry时,RingBuffer会将获取下一个可用的entry委托给Sequencer。我们以最简单的单生产者SingleProducerSequencer的next()实现来说明。SingleProducerSequencer.next()的源码如下:

    public long next(int n)
{
if (n < 1) //n表示此次生产者期望获取多少个序号,通常是1
{
throw new IllegalArgumentException("n must be > 0");
} long nextValue = this.nextValue; long nextSequence = nextValue + n; //生产者当前序号值+期望获取的序号数量后达到的序号值
long wrapPoint = nextSequence - bufferSize; //减掉RingBuffer的总的buffer值,用于判断是否出现‘覆盖’
long cachedGatingSequence = this.cachedValue; //从后面代码分析可得:cachedValue就是缓存的消费者中最小序号值,他不是当前最新的‘消费者中最小序号值’,而是上次程序进入到下面的if判定代码段是,被赋值的当时的‘消费者中最小序号值’
//这样做的好处在于:在判定是否出现覆盖的时候,不用每次都调用getMininumSequence计算‘消费者中的最小序号值’,从而节约开销。只要确保当生产者的节奏大于了缓存的cachedGateingSequence一个bufferSize时,从新获取一下 getMinimumSequence()即可。
//(wrapPoint > cachedGatingSequence) : 当生产者已经超过上一次缓存的‘消费者中最小序号值’(cachedGatingSequence)一个‘Ring’大小(bufferSize),需要重新获取cachedGatingSequence,避免当生产者一直在生产,但是消费者不再消费的情况下,出现‘覆盖’
//(cachedGatingSequence > nextValue) : 生产者和消费者均为顺序递增的,且生产者的seq“先于”消费者的seq,注意是‘先于’而不是‘大于’。当nextValue>Long.MAXVALUE时,nextValue+1就会变成负数,wrapPoint也会变成负数,这时候必然会是:cachedGatingSequence > nextValue
// 这个变化的过程会持续bufferSize个序号,这个区间,由于getMinimumSequence()得到的虽然是名义上的‘消费者中最小序号值’,但是不代表是走在‘最后面’的消费者
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
{
cursor.setVolatile(nextValue); // StoreLoad fence long minSequence;
//生产者停下来,等待消费者消费,知道‘覆盖’现象清除。
while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
{
waitStrategy.signalAllWhenBlocking();
LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
} this.cachedValue = minSequence;
} this.nextValue = nextSequence; return nextSequence;
}

批处理效应

当生产者节奏快于消费者,消费者可以通过‘批处理效应’快速追赶,即:消费者可以一次性从RingBuffer中获取多个已经准备好的enties,从而提高效率。代码实现如下:

SequenceBarrier的waitFor()方法:

    public long waitFor(final long sequence)
throws AlertException, InterruptedException, TimeoutException
{
checkAlert(); long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this); if (availableSequence < sequence)
{
return availableSequence;
} //获取消费者可以消费的最大的可用序号,支持批处理效应,提升处理效率。
//当availableSequence > sequence时,需要遍历 sequence --> availableSequence,找到最前一个准备就绪,可以被消费的event对应的seq。
//最小值为:sequence-1
return sequencer.getHighestPublishedSequence(sequence, availableSequence);
}

源代码

LMAX-Exchange源码github地址:https://github.com/LMAX-Exchange/disruptor

带中文注释的源码github地址:https://github.com/daoqidelv/disruptor

 

Disruptor3.0的实现细节的更多相关文章

  1. centos安装torch==1.4.0与相关细节

    对于某些直接安装torch==1.4.0报错的情况(没错,就是我遇到了) 在网上查找了,大概的解决方法是先安装一个低版本的torch和torchvision, torchvision是pytorch中 ...

  2. Disruptor学习笔记

    前言 以前一直听说有Disruptor这个东西,都说性能很强大,所以这几天自己也看了一下. 下面是自己的学习笔记,另外推荐几篇自己看到写的比较好的博客: Disruptor——一种可替代有界队列完成并 ...

  3. Disruptor学习笔记(一):基本原理和概念

    一.Disruptor基本原理 在多线程开发中,我们常常遇到这样一种场景:一些线程接受用户请求,另外一些线程处理这些请求.比如日志处理中的日志输入和告警.这种典型的生产者消费者场景十分常见,而生产者消 ...

  4. .NET Core 2.0版本预计于2017年春季发布

    英文原文: NET Core 2.0 Planned for Spring 2017 微软项目经理 Immo Landwerth 公布了即将推出的 .NET Core 2.0 版本的细节,该版本预计于 ...

  5. EF with (LocalDb)V11.0

    EF虽说对LocalDb支持的不错,但LocalDb有自身的缺陷(不想sqlite那样数据库文件可以像普通文件一样使用). LocalDb在一个计算机上会对数据库有唯一性约束,要求本机的localdb ...

  6. Asm Shader Reference --- Shader Model 2.0 part

    ps部分 ps_2_0 概览   Instruction Set                                   Name Description Instruction slot ...

  7. Codeforces Round #392 (Div. 2)-758D. Ability To Convert(贪心,细节题)

    D. Ability To Convert time limit per test 1 second Cmemory limit per test 256 megabytes input standa ...

  8. OAuth 2.0 RFC 框架 中文

    Internet Engineering Task Force (IETF) D. Hardt, Ed.Request for Comments: 6749 MicrosoftObsoletes: 5 ...

  9. 将0移到最后,在原数组操作,并且不能改变源数据顺序(JS编程)

    一.问题描述: 将0移到最后,在原数组操作,并且不能改变源数据顺序. 示例:输入:[2,0,0,1,0,3],  结果:[2,1,3,0,0,0] 二.问题分析与解决: 注意是在原数组上操作,不要进行 ...

随机推荐

  1. GTID复制详解

    前言 GTID复制是MySQL 5.6后的新功能,在传统的方式里,主从切换后,需要找到binlog和POS点,然后执行命令change master to 指向新的主库.对于不是很有经验的人来说,往往 ...

  2. iozone测试磁盘性能

    什么是iozone? iozone是一个文件系统的benchmark工具,可以测试不同的操作系统中文件系统的读写性能. 可以测试 Read, write, re-read,re-write, read ...

  3. [HTTP] PHP 实现 HTTP Server 原理

    单进程服务器简陋版: <?php /** * Single http server. * * Access http://127.0.0.1:8081 * * @license Apache-2 ...

  4. javaScript补充

    一.字符串常用的方法 obj.length 长度 obj.trim() 移除前后空白 obj.trimLeft() 移除前空白 obj.trimRight() 移除后空白 obj.charAt(n) ...

  5. python--关于赋值与深浅拷贝的认识

    作为一个自学python的小白,平时用到深浅拷贝的机会很少,因此对其也是一知半解.但是,作为一个立志成为后端工程狮的男人,眼里揉不了沙子,于是专门花时间补了补课,在此记录一下学习心得.    在讲深浅 ...

  6. 使用redis所维护的代理池抓取微信文章

    搜狗搜索可以直接搜索微信文章,本次就是利用搜狗搜搜出微信文章,获得详细的文章url来得到文章的信息.并把我们感兴趣的内容存入到mongodb中. 因为搜狗搜索微信文章的反爬虫比较强,经常封IP,所以要 ...

  7. c# 简单实现 插件模型 反射方式

    利用反射方式实现插件模型,wpf控件作为插件,然后用另外的窗体加载. 首先定义插件接口: public interface IUserControlLevel1 { string PluginName ...

  8. iOS-打电话、发短信、发邮件【指定QQ用户发送QQ消息】

    1.发短信 头文件 #import <MessageUI/MessageUI.h> 头部代理 @interface ViewController ()<MFMessageCompos ...

  9. BZOJ 3684: 大朋友和多叉树 [拉格朗日反演 多项式k次幂 生成函数]

    3684: 大朋友和多叉树 题意: 求有n个叶子结点,非叶节点的孩子数量\(\in S, a \notin S\)的有根树个数,无标号,孩子有序. 鏼鏼鏼! 树的OGF:\(T(x) = \sum_{ ...

  10. java windows自动化-mail自动发邮件

    本文旨在让测试人员了解如何发邮件 发邮件的话,最简单的事是直接手动发邮件,但是在自动化测试中,应做到让机器或者代码来自动发送邮件,笔者大概了解以下几种方法,总有一款口味适合你:1java代码来做下面即 ...