disruptor学习笔记

Ring Buffer的优势:

1. 内存屏障:volatile

提供与锁类似的语义,但是代码比锁小得多。volatile可以阻止代码重排序,并且值被更新的时候,会导致缓存失效,强制回写到主存中。


2. 伪共享与缓存行

CPU缓存通常一次缓存多个字(32、64或128字节),称为缓存行。如果位于同一个缓存行中的多个变量被不同的线程进行写,由于每次写都有可能刷新主存导致缓存失效,从而导致性能低下,即伪共享。可以使用缓存行填充来阻止共享缓存这种情况的出现,从而提升性能。如:

PaddedLong类,它使用了7个long,加上一个对象头,刚好64个字节。

Sequence类:private final long[] paddedValue = new long[15];

这个类更复杂一些,它的内存结构如下(从学习资料2中转载):

[头部:     8 bytes] 8
[数组长度: 4 bytes] 12 
[padding: 4 bytes] 16 
[元素1:    8 bytes] 24 
[元素2:    8 bytes] 32 
[元素3:    8 bytes] 40 
[元素4:    8 bytes] 48 
[元素5:    8 bytes] 56 
[元素6:    8 bytes] 64 
[元素7:    8 bytes] 72 
[元素8:    8 bytes] 80 
[元素9:    8 bytes] 88 
[元素10:   8 bytes] 96 
[元素11:   8 bytes] 104 
[元素12:   8 bytes] 112 
[元素13:   8 bytes] 120 
[元素14:   8 bytes] 128 
[元素15:   8 bytes] 136

Sequence使用了数组中第8个元素。这就意味着,无论在向头对齐还是向尾对齐,这个值总是会在一个单独的缓存行中。

有PaddedLong为什么还要Sequence类呢,主要是因为Java7对对象内存模型做了优化,如果它发现有字段没被使用,就会被优化掉,这时就又出现伪共享了。

见这篇博客:http://mechanical-sympathy.blogspot.com/2011/08/false-sharing-java-7.html


3. CAS:无锁更新

Compare and set,硬件直接支持的指令,ring buffer中通过UnSafe类来实现。


4. 大量的无锁实现(Sequence, PaddedLong等),使用数组存储数据(类似ArrayBlockingQueue),减少GC。


ring buffer整体结构:

一个环形的数组,next:下一个可用的slot。cursor:最近一个被填充的slot。

一个ClaimStrategy用于生产者生产数据时提供判断(当前是否有空闲的slot可以申请到),一个WaitStragety用于消费者消费数据时提供判断(当前是否有数据可以消费)。

其中ClaimStragety和WaitStragety都有针对不同场景的多种不同实现。

ClaimStragety:SingleThreadedClaimStragety,单线程;MultithreadedClaimStragety,多线程。


WaitStragety:

BlockingWaitStragety:使用了锁,实际上只能每个线程轮流来消费了。。。

BusySpinWaitStragety:使用自旋(会非常消耗CPU)

。。。


如何生产数据:

1. 取得当前可用的sequence

ringbuffer.next() ==> claimStragety.incrementAndGet() ==》waitForFreeSlotAt(依赖于消费者的sequence,即gatingSequences,每一个sequence都被一个BatchEventProcessor引用,代表最近消费的slot)。

看多个消费者中,消费得最慢的那个(即sequence最小的)如果minSequence > sequence-BufferSize,表示生产者和消费者重叠了,就需要等待。

2. 取得该sequence上的数据对象,填充对象

3. 发布数据。

claimStragety.serialisePublishing(sequence, cursor, batchSize);

waitStragety.signalAllWhenBlocking();

注意这里sequence是long的,cursor是Sequence类型的,位于Sequencer类中(RingBuffer的基类),是ring buffer维护的全局变量。

生产数据的时候,并不使用waitStragety,仅仅是在发布数据之后,会通知waitStragety,以激活潜在等待的消费者。

如何消费数据:
消费者是通过 ExecutorService,单独提交EventHandler的(如BatchEventHandler)。当然,它也引用了ringBuffer和sequenceBarrier(这个barrier用于多个ring buffer之间的依赖关系)。而Barrier依赖于WaitStragety,决定如何等待。
BatchEventHandler也非常简单,只是不停地循环,每次消费一个元素,如果没有元素可以消费就用适当的WaitStragety等待。消费完后更新自己的sequence。

消费数据的时候,也不使用claimStragety。


生产者和消费者相对独立,只通过ring buffer来交互。这样相对地减少了耦合。


附几个disruptor的学习资料:

1. http://ifeve.com/disruptor/ 这是关于disruptor的入门文章。

2. http://www.dongsm.cn/disruptor%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90-%E5%8F%96%E6%A8%A1%E7%9A%84%E4%BC%98%E5%8C%96/ 这里关于disruptor写了一系列,相对还比较深入。


你可能感兴趣的:(Java)