Disruptor进阶.docx
- 文档编号:1350938
- 上传时间:2022-10-21
- 格式:DOCX
- 页数:8
- 大小:21.27KB
Disruptor进阶.docx
《Disruptor进阶.docx》由会员分享,可在线阅读,更多相关《Disruptor进阶.docx(8页珍藏版)》请在冰豆网上搜索。
Disruptor进阶
Disruptor进阶
IntruductionDisruptor是Java实现的用于线程间通信的消息组件。
其核心是一个Lock-free的Ringbuffer。
我使用BlockingQueue与进Disruptor行了简单的对比测试,结果表明使用Disruptor来进行线程间通信效率会提高将近一倍。
而LMAX给出的数据是,使用Disruptor能够在一个线程里每秒处理6百万订单。
Disruptor为什么会如此快呢?
通过参考MartinFowler(Disruptor的开发者之一)的技术博客和Disruptor的源代码,可以总结出以下四条原因:
LockvsCASDisruptor使用CAS而不是Lock。
关于CAS(compareandswap)请参考WIKIPEDIA相关条目Compareandswap。
与大部分并发队列使用的Lock相比,CAS显然要快很多。
CAS是CPU级别的指令,更加轻量,不需要像Lock一样需要OS的支持,所以每次调用不需要kernelentry,也不需要contextswitch。
当然,使用CAS的代价是Disruptor实现的复杂程度也相对提高了。
避免伪共享现代计算机体系架构在各个层次都使用的Cache来提高效率,然而在多核体系结构中,对Cache的不恰当使用极易造成伪共享,使性能下降。
关于伪共享请参考WIKIPEDIA相关条目Falsesharing。
为了避免伪共享带来的性能下降,Disruptor对一切可能存在伪共享的地方使用Padding将两个不想关的内存隔离到两个缓存行上。
可能存在伪共享的地方包括两个不相关的线程共享变量之间,线程私有变量和线程共享变量之间等。
下面分别举例子说明。
在Disruptor的实现中,有一个多线程共享的计数组件Sequence,对Sequence的操作可以说是整个Disruptor的核心,关于Sequence,在下文介绍各个组件的时候还要详细说明。
这里主要说明它是怎样避免伪共享的。
主要代码如下:
staticfinallongINITIAL_VALUE=-1L;
privatestaticfinalUnsafeUNSAFE;
privatestaticfinallongVALUE_OFFSET;static
{
UNSAFE=Util.getUnsafe();
finalintbase=UNSAFE.arrayBaseOffset(long[].class);
finalintscale=UNSAFE.arrayIndexScale(long[].class);
VALUE_OFFSET=base+(scale*7);
}privatefinallong[]paddedValue=newlong[15];......publiclongget()
{
returnUNSAFE.getLongVolatile(paddedValue,VALUE_OFFSET);
}
Sequence定义了一个长度为15的long类型数组,但仅使用数组第八个元素进行计数,数组其他部分连同对象的头作为padding部分,保证在以64byte作为Cache缓存行大小的CPU中,计数元素(数组第八个元素)不会与其他变量存在于同一个缓存行中。
关于Java中对象在内存中具体怎样布局,可以参考深入理解Java虚拟机:
JVM高级特性与最佳实践另一个例子是关于线程私有变量的:
privatestaticclassPadding
{
/**Setto-1assequencestartingpoint*/
publiclongnextValue=Sequence.INITIAL_VALUE,cachedValue=Sequence.INITIAL_VALUE,p2,p3,p4,p5,p6,p7;
}privatefinalPaddingpad=newPadding();
这段代码用在单生产者的应用场景中。
在这种应用场景下,这个计数器不需要是线程安全的,使用Sequence过于heavy了,但仍然需要通过padding将其与其他线程共享的变量隔离开来。
变量p2到p7以及对象头的作用就是这个。
LinkedQueuevsArrayRingbufferDisruptor选择使用ArrayRingbuffer来构造lock-free队列,而不是选择LinkedQueue。
首先,数组是预分配的,这样不仅避免了JavaGC带来的运行开销,而且因为在ringbuffer上进行的操作是顺序执行的,所以对缓存来说更加友好,保证了缓存命中率。
使用Disruptor的时候,为了更好的利用Ringbuffer的这个优点,需要尽量将Ringbuffer的元素设计的可重用,生产者在生产消息或产生事件的时候对Ringbuffer元素中的属性进行更新,而不是替换Ringbuffer中的元素。
其次,数组在定位元素的时候是使用索引,而链表在定位元素的时候使用对象引用(地址)。
在lock-free队列中使用链表需要考虑使用Double-CAS等方式来克服ABA问题(关于double-CAS和ABA问题,请参coolshell上关于无锁队列的文章),而在数组中,因为元素是预分配的,所以不存在ABA问题。
Disruptor使用递增的Sequence来标示不同时刻访问的相同元素,比如一个消费者的Sequence等于i的时候表示在访问Ringbuffer的某个位置的元素,在下一次访问这个位置的元素的时候,Seqence等于i+buffer_size。
在需要访问数组元素的时候,只需要将序号对数组大小取余就可以得到数组索引。
每次对ringbuffer的访问都会导致相应的Sequence增加。
需要注意的是,由于Sequence是递增的,所以在到达最大值以后,会溢出,变成最小的负数,但这通常不是问题,因为要使long类型递增到溢出,即使每秒钟1000000000次递增,也需要上百年时间。
无时不刻的缓存为了高效,Disruptor可谓无所不用其极,它绝不会放过任何利用缓存的机会,看一个例子。
publiclongnext(intn)
{
if(n1)
{
thrownewIllegalArgumentException("nmustbe>0");
}longnextValue=pad.nextValue;longnextSequence=nextValue+n;
longwrapPoint=nextSequence-bufferSize;
longcachedGatingSequence=pad.cachedValue;if(wrapPoint>cachedGatingSequence||cachedGatingSequence>nextValue)
{
longminSequence;
while(wrapPoint>(minSequence=Util.getMinimumSequence(gatingSequences,nextValue)))
{
Thread.yield();
}pad.cachedValue=minSequence;
}pad.nextValue=nextSequence;returnnextSequence;
}
这个函数是在单生产者的应用场景下生产者获取n个可用元素时执行的代码。
在Disruptor里,需要多线程共享的序号,用Sequence表示,它是线程安全的,但访问Sequence的效率会因此降低,而在单线程内使用的序列号,是long类型,相对高效些。
得益于序列号是递增的,就可以使用long类型缓存访问Seqence的结果,优先使用缓存的序号,只有当缓存的序号不满足条件时,才去访问Sequence。
ComponentSequenceSequence是Disruptor最核心的组件,上面已经提到过了。
生产者对RingBuffer的互斥访问,生产者与消费者之间的协调以及消费者之间的协调,都是通过Sequence实现。
几乎每一个重要的组件都包含Sequence。
那么Sequence是什么呢?
首先Sequence是一个递增的序号,说白了就是计数器;其次,由于需要在线程间共享,所以Sequence是引用传递,并且是线程安全的;再次,Sequence支持CAS操作;最后,为了提高效率,Sequence通过padding来避免伪共享。
RingBufferRingBuffer是存储消息的地方,通过一个名为cursor的Sequence对象指示队列的头,协调多个生产者向RingBuffer中添加消息,并用于在消费者端判断RingBuffer是否为空。
巧妙的是,表示队列尾的Sequence并没有在RingBuffer中,而是由消费者维护。
这样的好处是多个消费者处理消息的方式更加灵活,可以在一个RingBuffer上实现消息的单播,多播,流水线以及它们的组合。
其缺点是在生产者端判断RingBuffer是否已满是需要跟踪更多的信息,为此,在RingBuffer中维护了一个名为gatingSequences的Sequence数组来跟踪相关Seqence。
SequenceBarrierSequenceBarrier用来在消费者之间以及消费者和RingBuffer之间建立依赖关系。
在Disruptor中,依赖关系实际上指的是Sequence的大小关系,消费者A依赖于消费者B指的是消费者A的Sequence一定要小于等于消费者B的Sequence,这种大小关系决定了处理某个消息的先后顺序。
因为所有消费者都依赖于RingBuffer,所以消费者的Sequence一定小于等于RingBuffer中名为cursor的Sequence,即消息一定是先被生产者放到Ringbuffer中,然后才能被消费者处理。
SequenceBarrier在初始化的时候会收集需要依赖的组件的Sequence,RingBuffer的cursor会被自动的加入其中。
需要依赖其他消费者和/或RingBuffer的消费者在消费下一个消息时,会先等待在SequenceBarrier上,直到所有被依赖的消费者和RingBuffer的Sequence大于等于这个消费者的Sequence。
当被依赖的消费者或RingBuffer的Sequence有变化时,会通知SequenceBarrier唤醒等待在它上面的消费者。
WaitStrategy当消费者等待在SequenceBarrier上时,有许多可选的等待策略,不同的等待策略在延迟和CPU资源的占用上有所不同,可以视应用场景选择:
BusySpinWaitStrategy:
自旋等待,类似LinuxKernel使用的自旋锁。
低延迟但同时对CPU资源的占用也多。
BlockingWaitStrategy:
使用锁和条件变量。
CPU资源的占用少,延迟大。
SleepingWaitStrategy:
在多次循环尝试不成功后,选择让出CPU,等待下次调度,多次调度后仍不成功,尝试前睡眠一个纳秒级别的时间再尝试。
这种策略平衡了延迟和CPU资源占用,但延迟不均匀。
YieldingWaitStrategy:
在多次循环尝试不成功后,选择让出CPU,等待下次调。
平衡了延迟和CPU资源占用,但延迟也比较均匀。
PhasedBackoffWaitStrategy:
上面多种策略的综合,CPU资源的占用少,延迟大。
BatchEvenProcessor在Disruptor中,消费者是以EventProcessor的形式存在的。
其中一类消费者是
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Disruptor 进阶