看过Disruptor代码之后的一些感想

这两天感冒休息在家。到下午精神渐好,于是继续开始翻看diruptor源代码(http://lmax-exchange.github.com/disruptor/)。目前的版本号还是disruptor-3.0.0-SNAPSHOT。整个diruptor给我的感觉不太像java风格,更像C++。看起来还是挺适应的。不过真的看进去,其中关键代码十分干净,的确写的十分精巧,藏了很多技巧。有些在粗看代码时有些地方还不太觉得。后来,自己想写一个类似的模型来比较一下。不做不打紧,一做才发觉原来原先很多知识还是似是而非的。很多时候真的是感觉说的不如实际去做。花了不少功夫后,诸多关窍才都豁然开朗,真是受益良多。要实现基于memory barrier的ringbuffer,其中技术多多。


大家对于CPU的乱序执行应该不会陌生((http://en.wikipedia.org/wiki/Out-of-order_execution)),面试题常会考,最多问的就是singleton的double check locking问题了。这是老掉牙的问题,但是要真正了解乱序执行对在多处理器环境下JVM的影响,那还是很不容易的。这里首先还是需要了解一下java的内存模型,也就是JSR-133规范(http://jcp.org/jsr/detail/133.jsp)。此外,再推荐一下新加坡国立大学(NUS)的一篇论文(http://www.comp.nus.edu.sg/~tulika/pact04.pdf),讲的十分透彻。内存一致性模型,首先要讲硬件内存模型(Hardware memory models),因为不同硬件内存模型,对于java 内存模型的实现由直接影响。最简单的硬件内存模型,就是著名Lamport老兄的顺序一致性模型(Sequential Consistency),也就是允许不同线程的操作可以通过任意顺序间插,但同个线程顺序是不变的。不过这种模型太简单,CPU和编译器无法基于SC模型做太多优化。要做更多优化,只有将模型放松一些,即主要4种Total Store Order (TSO), Partial Store Order (PSO), Weak Ordering (WO) and Release Consistency (RC)。想要具体了解的话,大家需要去看相关论文了。
S. Adve and K. Gharachorloo. Shared memory consistency models: A tutorial. IEEE Computer, pages 66–76, Dec. 1996.
D. Culler and J. P. Singh. Parallel Computer Architecture: A Hardware/Software Approach. Morgan Kaufmann Publishers, 1998


关键是看CPU对内存的操作(read/write/lock/unlock),有哪些是可以交换顺序,哪些是不行的。当然,这个Java内存模型都有严格规定。在特定的硬件内存模型下,为了满足java 内存模型的语义,有些乱序是不被允许的,需要插入memory barrier进行保护。这里需要对三类不同的共享变量的读写进行区分,即普通变量读写,volatile变量读写,以及Lock/unlock等操作。这些操作reorder的要求是不一样的。具体大家去看论文了。


对于内存数据顺序限制,可以分成以下几种:
load-load, load操作按代码顺序执行
load-store, 按照程序的先load再store方式执行
store-load, 按照程序的先store再load方式执行
store-store, store操作按代码顺序执行


看到这里,你可能已经会联想起discruptor中的Sequence类了。sequence体现了disrutpor中对ringbuffer操作的核心的思想,把所有对sequence的操作全封装好了,重要的memory barrier操作,都靠它实现。譬如,Sequence.setOrdered操作,注释是这样的:Perform an ordered write of this sequence.  The intent is a Store/Store barrier between this write and any previous store。它怎么实现的呢,哈哈,这就是调sun.misc.Unsafe了。其实有了开源代码,一切了无秘密。如果大家有兴趣,可以看看Unsafe JNI的gcc实现(http://www.oschina.net/code/explore/gcc-4.5.2/libjava/sun/misc/natUnsafe.cc),其实真正实现也是简单的很。有一点需要特别说明的,是Sequence类的cache line优化。所有对单个sequence对象变量的读写,都是需要按顺序的,而且会非常频繁。如果让这些对象和其他数据共享一个cache line,那很容易出现false sharing问题,性能会急剧下降。最好是能独占,或是尽少和其他数据共享cache line。Discruptor把单个sequence对象进行注水,一个本来只需要volatile long的变量,硬充到一个length为15个long型数组,加上java数组头上本身的8字节数据,恰好加起来128字节。


光理解Sequence实现,还不足以全面理解ringbuffer。有人就会很奇怪,为什么代码里RingBuffer没有带timeout的读写接口。其实很简单,它已经实现了很完整的不同的WaitStrategy。如果你真需要带timeout操作,那你完全可以用PhasedBackoffWaitStrategy来实现,它采用分段模式,你都可以指定spin lock的timeout,线程切换的timeout。有人就会很奇怪,为什么RingBuffer有trynext/tryPublishEvent接口,但不提供tryget呢?其实你自己实现tryget也很简单,只要消费者检查一下自己的GatingSequence的值,然后查一下BufferRing当前最新的游标,就大致能判断是否能get了。


在Disruptor的load test的例子,大部分需要在线程池下运行。自己写了一个wrapper,就提供了queue语义接口。为了和ArrayBlockingQueue做性能对比,还特别写了一个test driver,把一对多,多对一,多对多都测了一遍。在我笔记本上,Disruptor性能几乎是BlockingQueue中最快的ArrayBlockingQueue的一倍。Doug Lea老兄,不要生气呀。有一点需要说明的,其实Disruptor基于线程池写load test还是有道理的。这点一开始我也没有理解。后来几经波折,发觉数据总是不对,恍然间才顿悟,GatingSequence是每个消费线程都需要有一个的,共用的话会出大问题。另外,如果真的需要用,Ringbuffer的尺寸和线程池的大小要协调的好。如果不在线程池里用,在开始每个线程开始使用的时候,把自己的GatingSequence注册好。一旦该线程退出前,一定不要忘记把自己的GatingSequence给注销掉


前段时间,在哪里还看到过一个帖子,有人听Martin的现场演讲,就坐在哪个角落。当时感觉还有很多疑问,但印象最深的,就是Martin在讲,他们在生产上使用disruptor,只有两种情况,即ringbuffer里一直是空的,来了就被拿走了。要不就是多少兆的ringbuffer,一会儿就满了,结果发现往往是consumer处理发生了什么问题。ringbuffer本身可以用super fast来形容,从来没有慢在queue上。实际试下来,果然名不虚传。


最后想说一下,Disruptor的官网上(http://lmax-exchange.github.com/disruptor/)资料都很有技术含量。大家应该好好去读一下。我把QCon的视频看了两篇。

你可能感兴趣的:(Java的思考)