解读Disruptor系列--解读源码(0)之源码导读

本篇文章是后续解读Disruptor源码的导读,适合对Disruptor还不了解的同学。如果有兴趣,还可以看下我之前发的Disruptor系列文章。
要大概弄明白Disruptor是个什么玩意,可以先回答这几个问题:
Disruptor是什么?
为什么要用Disruptor?
Disruptor为什么那么快?

Disruptor是什么?

我也来个一句话总结:Disruptor是LMAX开源的、用于替代并发线程间数据交换的有界队列的、可选无锁的、高性能的线程间通讯框架。

听起来是不是感觉高大上?是不是觉得很好奇怎么个高性能?是不是还有点云里雾里咋交换数据?这也是我最初的感觉。
慢慢来,我们逐步分解这些名词。
Disruptor:首先我们看看Disruptor这个词。熟悉星际迷航(Star Trek)的同学可能对这个词会更熟悉,Disruptor(裂解炮)和Phaser(相位枪)都是星际迷航中的武器,Phaser是联邦武器,而Disruptor是克林贡的等价物。看起来Disruptor和Phaser是类似的,那Phaser又是啥呢?在jdk1.7中,Doug Lea大师为我们带来了Phaser,一个可重用的同步屏障,功能类似CyclicBarrier和CountDownLatch,但是使用更加灵活。
LMAX团队之所以为Disruptor起了这个名字,一方面是因为Disruptor和Phaser在处理依赖图表(或者说是消费链、多阶段处理、流水线)时有类似的地方;另一方面,Disruptor这个词本身就有分裂、破坏的意思,很符合Disruptor中的设计思想对于传统并发编程思想的某种“对立”--并发编程总是在想如何利用多线程去拥有更大的吞吐量、更少的延迟,而Disruptor却通过非常多的性能测试发现,在并发编程中正是过多的线程间通讯、争抢导致了性能瓶颈,而最终选择了单线程的处理逻辑去完成业务处理。
LMAX:LMAX是英国一个交易量很大的金融交易所,也是开源Disruptor的组织。
开源:https://github.com/LMAX-Exchange/disruptor
并发线程间数据交换的有界队列 :在并发编程时,经常需要在线程间交换数据,如任务分发、事件传递、状态变更等, 这时通常需要多个线程去访问一个线程安全的数据结构,一般选择使用队列(Queue)实现。队列在容量上又分为有界和无界,使用无界队列通常都需要保证生产者生产速度小于消费者消费速度,否则将由于内存耗尽导致灾难结果。为了避免这种情况,队列通常是有界的。Java中经常使用BlockingQueue作为并发线程使用的有界队列,使用put()、take()在队列满、空的情况下进行等待,非常适合多线程间的数据共享,实现方式一般有ArrayBlockingQueue和LinkedBlockingQueue。
线程间通讯:等同于并发线程间的数据交换。
可选无锁:使用Disruptor唯一可能遇到Java锁的时候,就是在消费者等待可用事件进行消费时。而Disruptor为这个等待过程,编写了包括使用锁和不使用锁的多种策略,可根据不同场景和需求进行选择。
高性能:单生产者+单消费者性能测试:
使用LinkedBlockingQueue(https://github.com/LMAX-Exchange/disruptor/blob/master/src/perftest/java/com/lmax/disruptor/queue/OneToOneQueueThroughputTest.java )

Starting Queue tests
Run 0, BlockingQueue=3,687,315 ops/sec
Run 1, BlockingQueue=2,721,088 ops/sec
Run 2, BlockingQueue=3,471,017 ops/sec
Run 3, BlockingQueue=5,347,593 ops/sec
Run 4, BlockingQueue=5,316,321 ops/sec
Run 5, BlockingQueue=5,449,591 ops/sec
Run 6, BlockingQueue=5,173,305 ops/sec

使用常用的带Translator的Disruptor测试(https://github.com/LMAX-Exchange/disruptor/blob/master/src/perftest/java/com/lmax/disruptor/translator/OneToOneTranslatorThroughputTest.java)

Starting Disruptor tests
Run 0, Disruptor=20,222,446 ops/sec
Run 1, Disruptor=70,671,378 ops/sec
Run 2, Disruptor=37,878,787 ops/sec
Run 3, Disruptor=37,105,751 ops/sec
Run 4, Disruptor=38,986,354 ops/sec
Run 5, Disruptor=27,578,599 ops/sec
Run 6, Disruptor=34,281,796 ops/sec

综上的名词解释,聪明如你,想必已经明白个大概了。如果还是不明白,肯定是我讲的不好,请留言。

为什么要用Disruptor?

那为什么要用Disruptor呢?或者说Disruptor解决了什么问题呢?
一般情况下,新的发明创造都是伴随着对旧有事物的痛点产生的。在并发线程间交换数据这个问题上,使用传统的阻塞队列有什么问题呢?
根据LMAX团队在2011年发布的论文,可简单归结为以下几个问题:

  1. 锁的成本: 传统阻塞队列使用锁保证线程安全。而锁通过操作系统内核的上下文切换实现,会暂停线程去等待锁直到释放。执行这样的上下文切换,会丢失之前保存的数据和指令。由于消费者和生产者之间的速度差异,队列总是接近满或者空的状态。这种状态会导致高水平的写入争用。
  2. 伪共享问题导致的性能低下。
  3. 队列是垃圾的重要来源,队列中的元素和用于存储元素的节点对象需要进行频繁的重新分配。

论文中给出的,循环5亿次64位计数操作使用时间对比:

解读Disruptor系列--解读源码(0)之源码导读_第1张图片
执行5亿次64位计数操作使用时间对比

Disruptor通过良好的设计,最大限度解决了上述三个问题。
好奇如你,难道就不想知道其中奥妙吗?

Disruptor为什么那么快?

简单来说,Disruptor在设计上有以下几点优势:

  1. 内部数据存储使用环形缓冲(Ring Buffer),在启动时进行对象内存分配,这个对象并非数据本身,而只是一个数据容器。这个容器由用户提供,在Disruptor运行时,生产者负责拿到容器设置好数据,消费者再去可用的容器中拿到数据完成消费。这样分配对象内存,将有极大可能让这些对象内存在主存中连续分配,从而支持了CPU缓存位置预测。否则每次new一个对象,很难知道对象内存分配到哪了。这样做好有个好处,环形缓冲在JVM生命周期中通常是永生的,GC的压力更小
  2. 尽量使用无锁设计,合理使用CAS。举两个例子。
    a. 通过区分是否允许并发生产者,细分单生产者模式和多生产者模式,单生产者单线程更新游标,多生产者模式使用CAS更新游标位置。
    b. 在阻塞型等待策略中,将等待分两部分。第一部分,消费者等待生产者发布新数据,由于不确定等待时间,使用锁的条件等待。而第二部分,消费者等待上一组消费者(Disruptor支持链式消费,类似Phaser分阶段处理) 完成此数据消费时,由于已确定有可消费数据,且假定通常的消费时间较短,所以使用自旋(忙循环)来避免上下文切换导致的性能开销。能不用锁就不用锁,即便要用锁,也要将使用锁的粒度用的最小。
  3. 优化数据结构,解决伪共享问题。Java中通过填充缓存行,来解决伪共享问题的思路,现在可能已经是老生常谈,连Java8中都新增了sun.misc.Contended注解来避免伪共享问题。但在Disruptor刚出道那会儿,能够以“机械同情(mechanical sympathy)”的思考方式,来优化Java数据结构,这恐怕还很新潮。
  4. 优化其他实现细节。举几个例子。
    a. 如一个RingBuffer,容量bufferSize为2的幂。使用sequence表示事件序号,可通过sequence & (bufferSize - 1)定位元素的index,比普通的求余取模(%)要快得多。事先计算好bufferSize以2为底的对数indexShift,可通过sequence >>> indexShift快速计算出sequence/bufferSize的商flag(其实相当于当前sequence在环形跑道上跑了几圈,在数据生产时要设置好flag,在数据消费时就可以通过比对flag判断此位置的数据是不是本圈的数据),比除法要快得多。
    b. 合理使用Unsafe,实现更加高效地内存管理和原子访问。Unsafe封装了一些类似C/C++中的指针操作,除了JDK,在Netty、Spring、Kafka、Storm等非常多的流行开源项目中都使用了Unsafe。

简单讲到这,后续开始分享解读Disruptor源码的内容。


参考资料
1.Java8使用@sun.misc.Contended避免伪共享 - http://www.jianshu.com/p/c3c108c3dcfd
2.写Java也得了解CPU--CPU缓存 http://www.cnblogs.com/techyc/p/3607085.html
3.linux查看CPU高速缓存(cache)信息 http://www.cnblogs.com/kekukele/p/3829369.html
4.理解 CPU Cache http://wsfdl.com/linux/2016/06/11/%E7%90%86%E8%A7%A3CPU%E7%9A%84cache.html
5.关于CPU Cache -- 程序猿需要知道的那些事 http://cenalulu.github.io/linux/all-about-cpu-cache/
6.Mechanical Sympathy - Memory Barriers/Fences https://mechanical-sympathy.blogspot.tw/2011/07/memory-barriersfences.html
7.《Java高并发程序设计》- 4.4.3 Java中的指针:Unsafe类

你可能感兴趣的:(解读Disruptor系列--解读源码(0)之源码导读)