Disruptor介绍(一)

转载自:http://www.cnblogs.com/haiq/p/4112689.html
             http://developer.51cto.com/art/201306/399370.htm
             http://www.th7.cn/Program/java/201410/289634.shtml

一、什么是 Disruptor

       简单的说,Disruptor是一个高性能的Buffer,并提供了使用这个Buffer的框架。
       从功能上来看,Disruptor 是实现了“队列”的功能,而且是一个有界队列。JDK的BlockingQueue是一个 FIFO 队列,生产者(Producer)往队列里发布(publish)一项事件(或称之为“消息)时,消费者(Consumer)能获得通知;如果没有事件时,消费者被堵塞,直到生产者发布了新的事件。

       这些都是 Disruptor 能做到的,与之不同的是,Disruptor 能做更多:
       • 同一个“事件”可以有多个消费者,消费者之间既可以并行处理,也可以相互依赖形成处理的先后次序(形成一个依赖图);
       • 预分配用于存储事件内容的内存空间;
       • 针对极高的性能目标而实现的极度优化和无锁的设计;
       一般性地来说,当需要在两个独立的处理过程(两个线程)之间交换数据时,就可以使用Disruptor 。当然使用队列(如上面提到的 BlockingQueue)也可以,只不过 Disruptor 做得更好。

二、Disruptor为什么速度更快?

       BlockingQueue使用的是package java.util.concurrent.locks中实现的锁,当多个线程(例如生产者)同时写 入Queue时,锁的争抢会导致只有一个生产者可以执行,其他线程都中断了,也就是线程的状态从RUNNING切换到BLOCKED,直到某个生产者线程 使用完Buffer后释放锁,其他线程状态才从BLOCKED切换到RUNNABLE,然后时间片到其他线程后再进行锁的争抢。上述过程中,一般来说生产者存放一个数据到Buffer中所需时间是非常短的,操作系统切换线程上下文的速度也是非常快的,但是当线程数量增多后,OS切换线程所带来的开销逐渐增多,锁的反复申请和释放成为性能瓶颈。

       BlockingQueue除了使用锁带来的性能损失外,还可能因为线程争抢的顺序问题造成性能再次损失:实际使 用中发现线程的调度顺序并不理想,可能出现短时间内OS频繁调度出生产者或消费者的情况,这样造成缓冲区可能短时间内被填满或被清空的极端情况。

       上面的问题Disruptor的解决方案是:不用锁。

三、Ringbuffer

       Disruptor使用一个Ring Buffer存放生产者的“产品”,环形缓冲区实际上还是一段连续内存,之所以称作环形是因为它对数据存放位置的处理,生产者和消费者各有一个指针(数组 下标),消费者的指针指向下一个要读取的Slot,生产者指针指向下一个要放入的Slot,消费或生产后,各自的指针值p = (p +1) % n,n是缓冲区长度,这样指针在缓冲区上反复游走,故可以将缓冲区看成环状。如图:
                                    Disruptor介绍(一)_第1张图片
 
       基本来说,ringbuffer拥有一个序号,这个序号指向数组中下一个可用的元素。随着不停地填充这个buffer(可能也会有相应的读取),这个序号会一直增长,直到绕过这个环。要找到数组中当前序号指向的元素,可以通过mod操作。与环形buffer相较,ringbuffer与其最大的区别在于:没有尾指针,只维护了一个指向下一个可用位置的序号。选择用环形buffer的最初原因就是想要提供可靠的消息传递。需要将已经被服务发送过的消息保存起来,这样当另外一个服务通过nak (拒绝应答信号)告诉没有成功收到消息时,能够重新发送。
       实现的ring buffer和常用的队列之间的区别是,不删除buffer中的数据,也就是说这些数据一直存放在buffer中,直到新的数据覆盖。
       因为它是数组,所以要比链表快,而且有一个容易预测的访问模式。这是对CPU缓存友好的—也就是说,在硬件级别,数组中的元素是会被预加载的,因此在ringbuffer当中,cpu无需时不时去主存加载数组中的下一个元素。其次,可以为数组预先分配内存,使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收。此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。

四、使用Ring Buffer

       当生产者和消费者都只有一个时,由于两个线程分别操作不同的指针,所以不需要锁。
       当有多个消费者时,(按Disruptor的设计)每个消费者各自控制自己的指针,依次读取每个Slot(也就是每个消费者都会读取到所有的产品),这时只需要保证生产者指针不会超过最慢的消费者(超过最后一个消费者“一圈”)即可,也不需要锁。
       当有多个生产者时,多个线程共用一个写指针,此处需要考虑多线程问题,例如两个生产者线程同时写数据,当前写指针=0,运行后其中一个线程应获得缓冲区0号Slot,另一个应该获得1号,写指针=2。对于这种情况,Disruptor使用CAS来保证多线程安全。

具体分析:
       消费者(Consumer)是一个想从Ring Buffer里读取数据的线程,它可以访问ConsumerBarrier对象,这个对象由RingBuffer创建并且代表消费者与RingBuffer进行交互。就像Ring Buffer显然需要一个序号才能找到下一个可用节点一样,消费者也需要知道它将要处理的序号,每个消费者都需要找到下一个它要访问的序号。例如,消费者处理完了Ring Buffer里序号8之前(包括8)的所有数据,那么它期待访问的下一个序号是9。
       ConsumerBarrier(这个类在2.0.0之后就一直被改名,3.2.1的版本中它是SequenceBarrier)返回RingBuffer的最大可访问序号,假设是12。ConsumerBarrier有一个WaitStrategy方法来决定它如何等待这个序号。接下来,消费者会一直原地停留,等待更多数据被写入Ring Buffer。并且,一旦数据写入后消费者会收到通知,节点9,10,11和12已写入。现在序号12到了,消费者可以让ConsumerBarrier去拿这些序号节点里的数据了。拿到了数据后,消费者(Consumer)会更新自己的标识(cursor)。

       这样做是怎样有助于平缓延迟的峰值的呢?以前需要逐个节点地询问“我可以拿下一个数据吗?现在可以了么?现在呢?”,消费者(Consumer)现在只需要简单的说“当你拿到的数字比我这个要大的时候请告诉我”,函数返回值会告诉它有多少个新的节点可以读取数据了。因为这些新的节点的确已经写入了数据(Ring Buffer本身的序号已经更新),而且消费者对这些节点的唯一操作是读而不是写,因此访问不用加锁。不仅代码实现起来可以更加安全和简单,而且不用加锁使得速度更快。
       另一个好处是可以用多个消费者(Consumer)去读同一个RingBuffer,不需要加锁,也不需要用另外的队列来协调不同的线程(消费者)。这样可以在Disruptor的协调下实现真正的并发数据处理。

       Ring Buffer还是与消费端一样提供了一个ProducerBarrier对象,让生产者通过它来写入Ring Buffer。写入Ring Buffer的过程涉及到两阶段提交。首先,生产者需要申请buffer里的下一个节点。然后,当生产者向节点写完数据,它将会调用ProducerBarrier的commit方法。
       首先来看看第一步。“给我Ring Buffer里的下一个节点”,这句话听起来很简单。的确,从生产者角度来看它很简单:简单地调用ProducerBarrier的nextEntry()方法,这样会返回给一个Entry对象(3.2.1后Entry对象是用Event事件来代替),这个对象就是 Ring Buffer 的下一个节点。

ProducerBarrier 如何防止 Ring Buffer 重叠
       在后台,由 ProducerBarrier 负责所有的交互细节来从Ring Buffer中找到下一个节点,然后才允许生产者向它写入数据。
       ConsumerTrackingProducerBarrier对象拥有所有正在访问Ring Buffer的消费者列表。因为不想与队列“混为一谈”(队列需要追踪队列的头和尾,它们有时候会指向相同的位置),Disruptor由消费者负责通知它们处理到了哪个序列号,而不是 Ring Buffer。所以,如果想确定没有让Ring Buffer重叠,需要检查所有的消费者们都读到了哪里。
       例如,有一个消费者顺利的读到了最大序号12。第二个消费者有点落后,它停在序号3。现在生产者想要写入Ring Buffer中序号3占据的节点,因为它是Ring Buffer当前游标的下一个节点。但是ProducerBarrier明白现在不能写入,因为有一个消费者正在占用它。所以,ProducerBarrier 停下来自旋 (spins),等待,直到那个消费者离开。现在可以想像消费者2已经处理完了一批节点,并且向前移动了它的序号。可能它挪到了序号 9,ProducerBarier会看到下一个节点,序号 3 那个已经可以用了。它会抢占这个节点上的 Entry(Entry 对象,基本上它是一个放写入到某个序号的 Ring Buffer 数据的桶),把下一个序号(13)更新成 Entry 的序号,然后把 Entry 返回给生产者。生产者可以接着往 Entry 里写入数据。
       当生产者结束向 Entry 写入数据后,它会要求 ProducerBarrier 提交。ProducerBarrier 先等待 Ring Buffer 的游标追上当前的位置(对于单生产者这毫无意义,比如,我们已经知道游标到了12 ,而且没有其他人正在写入 Ring Buffer)。然后 ProducerBarrier更新Ring Buffer的游标到刚才写入的Entry 序号13。接下来,ProducerBarrier 会让消费者知道 buffer 中有新东西了。它戳一下 ConsumerBarrier 上的 WaitStrategy 对象说-“喂,醒醒!有事情发生了!”(注意-不同的 WaitStrategy 实现以不同的方式来实现提醒,取决于它是否采用阻塞模式。)












你可能感兴趣的:(Disruptor介绍(一))