vlambda博客
学习文章列表

Storm、Log4j2高性能之—Disruptor队列

Storm、Log4j2高性能之—Disruptor队列

1. Disruptor简介

Disruptor(https://github.com/LMAX-Exchange/disruptor)是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与I/O操作处于同样的数量级)。基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。2011年,企业应用软件专家Martin Fowler专门撰写长文介绍。同年它还获得了Oracle官方的Duke大奖。

像Storm、Log4j 2在内的很多知名项目都应用了Disruptor以获取高性能。

需要特别说明的是这里所说的队列是系统内部的内存队列,其内部为大数数组实现的环形队列(*RingBuffer*),而不是Kafka这样的分布式队列。

既然是Storm、Log4j2等将其作为内存队列使用,那么自然会有这几个疑问:

(1)为什么不直接使用java语言的内置队列呢?

(2)相比Java的内置队列,Disruptor的优势在哪?

(3)Disruptor是怎么样比java内置队列拥有优势的呢?

接下来我们将主要从Disruptor本身的实现策略的关键点及Java内置队列的实现进行比较总结,而不过多注重代码层面的解读。

2. Java内置队列

首先我们需要看一下Java的内置队列,如下所示。

队列 有界性 数据结构
ArrayBlockingQueue bounded 加锁 arraylist
LinkedBlockingQueue optionally-bounded 加锁 linkedlist
ConcurrentLinkedQueue unbounded 无锁 linkedlist
LinkedTransferQueue unbounded 无锁 linkedlist
PriorityBlockingQueue unbounded 加锁 heap
DelayQueue unbounded 加锁 heap

2.1 Java内置队列的数据结构

一般可以总结为三类:数组、链表和堆。其中,堆一般情况下是为了实现带有优先级特性的队列,此博文中,我们只关心无优先级的队列操作,所以此处不做过多赘述,仅从数组和链表两种数据结构来看:基于数组线程安全的队列,其中最典型的就是ArrayBlockingQueue,它主要通过加锁的方式来保证线程安全;基于链表的线程安全队列分成LinkedBlockingQueue和ConcurrentLinkedQueue两大类,前者也通过锁的方式来实现线程安全,而后者以及上面表格中的LinkedTransferQueue都是通过原子变量compare and swap(以下简称“CAS”)这种不加锁的方式来实现的。通过不加锁的方式实现的队列都是无界的(无法保证队列的长度在确定的范围内);而加锁的方式,可以实现有界队列。在稳定性要求特别高的系统中,为了防止生产者速度过快,导致内存溢出,只能选择有界队列;同时,为了减少Java的垃圾回收对系统性能的影响,会尽量选择array/heap格式的数据结构。这样筛选下来,符合条件的队列就只有ArrayBlockingQueue。ArrayBlockingQueue在实际使用过程中,会因为加锁和同现代CPU更底层设计而相悖的编码理论产生的伪共享等出现严重的性能问题。

2.2加锁

 现实编程过程中,加锁通常会严重地影响性能。线程会因为竞争不到锁而被挂起,等锁被释放的时候,线程又会被恢复,这个过程中存在着很大的开销,并且通常会有较长时间的中断,因为当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行,例如发生了缺页错误、调度延迟或者其它类似情况,那么所有需要这个锁的线程都无法执行下去。如果被阻塞线程的优先级较高,而持有锁的线程优先级较低,就会发生优先级反转,所以相比之下锁的坏处,不仅局限于性能的开销,更加危险的是安全问题的隐患!  Disruptor论文中讲述了一个实验: 这个测试程序调用了一个函数,该函数会对一个64位的计数器循环自增5亿次。 机器环境:2.4G 6核  运算:64位的计数器累加5亿次

Method Time (ms)
Single thread 300
Single thread with CAS 5,700
Single thread with lock 10,000
Single thread with volatile write 4,700
Two threads with CAS 30,000
Two threads with lock 224,000

 CAS操作比单线程无锁慢了1个数量级;有锁且多线程并发的情况下,速度比单线程无锁慢3个数量级。可见无锁速度最快。 单线程情况下,不加锁的性能 > CAS操作的性能 > 加锁的性能。 在多线程情况下,为了保证线程安全,必须使用CAS或锁,这种情况下,CAS的性能超过锁的性能,前者大约是后者的8倍。 综上可知,加锁的性能相对来说是最差的。

3. 锁和CAS及内存屏障与线程安全

保证线程安全一般分成两种方式:锁和原子变量。

3.1 锁  

 

图1 通过加锁的方式实现线程安全  


采取加锁的方式,即是最无奈的处理方式(悲观锁),默认线程会冲突,访问数据时,先加上锁再访问,访问之后再解锁。通过锁界定一个临界区,同时只有一个线程进入。如上图所示,Thread2访问Entry的时候,加了锁,Thread1就不能再执行访问Entry的代码,从而保证线程安全。

ArrayBlockingQueue通过加锁的方式实现的offer方法,保证线程安全。

3.2 原子变量

原子变量能够保证原子性的操作,意思是某个任务在执行过程中,要么全部成功,要么全部失败回滚,恢复到执行之前的初态,不存在初态和成功之间的中间状态。例如CAS操作,要么比较并交换成功,要么比较并交换失败。由CPU保证原子性。

通过原子变量可以实现线程安全。执行某个任务的时候,先假定不会有冲突,若不发生冲突,则直接执行成功;当发生冲突的时候,则执行失败,回滚再重新操作,直到不发生冲突。  Storm、Log4j2高性能之—Disruptor队列  



图2 通过原子变量CAS实现线程安全  


如图所示,Thread1和Thread2都要把Entry加1。若不加锁,也不使用CAS,有可能Thread1取到了myValue=1,Thread2也取到了myValue=1,然后相加,Entry中的value值为2。这与预期不相符,我们预期的是Entry的值经过两次相加后等于3。

CAS会先把Entry现在的value跟线程当初读出的值相比较,若相同,则赋值;若不相同,则赋值执行失败。一般会通过while/for循环来重新执行,直到赋值成功。

值得重点提及的是CAS和MemoryBarrier一样,是CPU级别的指令,后者的实现是java中对应的谜一样的volatile关键字。

在高度竞争的情况下,锁的性能将超过原子变量的性能,但是更真实的竞争情况下,原子变量的性能将超过锁的性能。更加值得祝贺的是与此同时原子变量不会有死锁等灾难性问题。

3.3 内存屏障

它是一个CPU指令。没错,又一次,我们在讨论CPU级别的东西,以便获得我们想要的性能(Martin著名的Mechanical Sympathy理论)。基本上,它是这样一条指令:a)确保一些特定操作执行的顺序;b)影响一些数据的可见性(可能是某些指令执行后的结果)。

编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。   Storm、Log4j2高性能之—Disruptor队列  

内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

内存屏障和Java有什么关系?

现在我知道你在想什么——这不是汇编程序。它是Java。

这里有个神奇咒语叫volatile(我觉得这个词在Java规范中从未被解释清楚)。如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。   Storm、Log4j2高性能之—Disruptor队列  

这意味着如果你对一个volatile字段进行写操作,你必须知道:

1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。

2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

内存屏障对性能的影响

内存屏障作为另一个CPU级的指令,没有锁那样大的开销。内核并没有在多个线程间干涉和调度。但凡事都是有代价的。内存屏障的确是有开销的——编译器/cpu不能重排序指令,导致不可以尽可能地高效利用CPU,另外刷新缓存亦会有开销。所以不要以为用volatile代替锁操作就一点事都没。正所谓没有最好的,只有最适合的!相比之下内存屏障屏蔽掉一些指令重排序优化带来的开销代价远远小于锁而已!

4. 伪共享

4.1 什么是共享

下图是计算的基本结构。L1、L2、L3分别表示一级缓存、二级缓存、三级缓存,越靠近CPU的缓存,速度越快,容量也越小。所以L1缓存很小但很快,并且紧靠着在使用它的CPU内核;L2大一些,也慢一些,并且仍然只能被一个单独的CPU核使用;L3更大、更慢,并且被单个插槽上的所有CPU核共享;最后是主存,由全部插槽上的所有CPU核共享。  Storm、Log4j2高性能之—Disruptor队列  


图3 计算机CPU与缓存示意图  

当CPU执行运算的时候,它先去L1查找所需的数据、再去L2、然后是L3,如果最后这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要尽量确保数据在L1缓存中。

另外,线程之间共享一份数据的时候,需要一个线程把数据写回主存,而另一个线程访问主存中相应的数据。

下面是从CPU访问不同层级数据的时间概念:  

从CPU到 大约需要的CPU周期 大约需要的时间
主存
约60-80ns
QPI 总线传输(between sockets, not drawn)
约20ns
L3 cache 约40-45 cycles 约15ns
L2 cache 约10 cycles 约3ns
L1 cache 约3-4 cycles 约1ns
寄存器 1 cycle

可见CPU读取主存中的数据会比从L1中读取慢了近2个数量级。

4.2 缓存行

CPU每次从主存中拉取数据时,会把相邻的数据也存入同一个cache line。

在访问一个long数组的时候,如果数组中的一个值被加载到缓存中,它会自动加载另外7个。因此你能非常快的遍历这个数组。事实上,你可以非常快速的遍历在连续内存块中分配的任意数据结构。


4.3 伪共享及其解决手段

ArrayBlockingQueue有三个成员变量:- takeIndex:需要被取走的元素下标 - putIndex:可被元素插入的位置的下标 - count:队列中元素的数量

这三个变量很容易放到一个缓存行中,但是之间修改没有太多的关联。所以每次修改,都会使之前缓存的数据失效,从而不能完全达到共享的效果。  Storm、Log4j2高性能之—Disruptor队列  图4 ArrayBlockingQueue伪共享示意图  


如上图所示,当生产者线程put一个元素到ArrayBlockingQueue时,putIndex会修改,从而导致消费者线程的缓存中的缓存行无效,需要从主存中重新读取。

这种无法充分使用缓存行特性的现象,称为伪共享,这里我们可以理解为最小缓存的内容由于某种原因或者cache大小的对齐方式导致的同主存中变化不一致而需要重新刷新CacheLine的情况。

对于伪共享,一般的解决方案是,增大数组元素的间隔使得由不同线程存取的元素位于不同的缓存行上,以空间换时间(兜兜转转,终究要回到最初的起点OS和计算机组成原理的层面来优化代码可以提升的空间,即文中提到的以空间换时间)。

以下为对应的Disruptor实现的部分填充代码截取。

package com.lmax.disruptor;

import sun.misc.Unsafe;

import com.lmax.disruptor.util.Util;


class LhsPadding
{
  protected long p1, p2, p3, p4, p5, p6, p7;
}

class Value extends LhsPadding
{
  protected volatile long value;
}

class RhsPadding extends Value
{
  protected long p9, p10, p11, p12, p13, p14, p15;
}

/**
* <p>Concurrent sequence class used for tracking the progress of
* the ring buffer and event processors. Support a number
* of concurrent operations including CAS and order writes.
*
* <p>Also attempts to be more efficient with regards to false
* sharing by adding padding around the volatile field.
*/
public class Sequence extends RhsPadding...


值得特别说明的是,早在jdk1.8中,有专门的注解@Contended来避免伪共享,更优雅地解决问题。

5. Disruptor的加速措施

Disruptor通过以下设计来解决队列速度慢的问题:

  • 环形数组结构

    为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好。

    元素位置定位,数组长度2^n,通过位运算,相取模运算加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。    index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。

    与此同时,只需要 维护一个队尾指针即可。减少了队头队尾在多N生产者和N消费者条件下的竞争问题。

  • 竞争点的分离和合并策略

    代码层面的Strategy和RingBuffer的维护中可以较为明显的体现出来,此处不做展开。

    竞争点采用cacheline填充及细粒度的Java volatile和CAS

  • 无锁设计

每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。

下面忽略数组的环形结构,介绍一下如何实现无锁设计。整个过程通过原子变量CAS,保证操作的线程安全,不要让 Ring 重叠;如何通知消费者;生产者一端的批处理;以及多个生产者如何协同工作。

5.1 生产者及ProducerBarriers组件

Disruptor 代码 给 消费者 提供了一些接口和辅助类,但是没有给写入 Ring Buffer 的 生产者 提供接口。这是因为除了你需要知道生产者之外,没有别人需要访问它。尽管如此,Ring Buffer 还是与消费端一样提供了一个 ProducerBarrier 对象,让生产者通过它来写入 Ring Buffer。

写入 Ring Buffer 的过程涉及到两阶段提交 (two-phase commit)。首先,你的生产者需要申请 buffer 里的下一个节点。然后,当生产者向节点写完数据,它将会调用 ProducerBarrier 的 commit 方法。

那么让我们首先来看看第一步。“给我 Ring Buffer 里的下一个节点”,这句话听起来很简单。的确,从生产者角度来看它很简单:简单地调用 ProducerBarrier 的 nextEntry() 方法,这样会返回给你一个 Entry 对象,这个对象就是 Ring Buffer 的下一个节点。

5.1.2 ProducerBarrier 如何防止 Ring Buffer 重叠

在后台,由 ProducerBarrier 负责所有的交互细节来从 Ring Buffer 中找到下一个节点,然后才允许生产者向它写入数据。  Storm、Log4j2高性能之—Disruptor队列  

在这幅图中,我们假设只有一个生产者写入 Ring Buffer。过一会儿我们再处理多个生产者的复杂问题。

ConsumerTrackingProducerBarrier 对象拥有所有正在访问 Ring Buffer 的 消费者 列表。这看起来有点儿奇怪-我从没有期望 ProducerBarrier 了解任何有关消费端那边的事情。但是等等,这是有原因的。因为我们不想与队列“混为一谈”(队列需要追踪队列的头和尾,它们有时候会指向相同的位置),Disruptor 由消费者负责通知它们处理到了哪个序列号,而不是 Ring Buffer。所以,如果我们想确定我们没有让 Ring Buffer 重叠,需要检查所有的消费者们都读到了哪里。

在上图中,有一个 消费者 顺利的读到了最大序号 12(用红色/粉色高亮)。第二个消费者 有点儿落后——可能它在做 I/O 操作之类的——它停在序号 3。因此消费者 2 在赶上消费者 1 之前要跑完整个 Ring Buffer 一圈的距离。

现在生产者想要写入 Ring Buffer 中序号 3 占据的节点,因为它是 Ring Buffer 当前游标的下一个节点。但是 ProducerBarrier 明白现在不能写入,因为有一个消费者正在占用它。所以,ProducerBarrier 停下来自旋 (spins),等待,直到那个消费者离开。

5.1.3 申请下一个节点

现在可以想像消费者 2 已经处理完了一批节点,并且向前移动了它的序号。可能它挪到了序号 9(因为消费端的批处理方式,现实中我会预计它到达 12,但那样的话这个例子就不够有趣了)。  Storm、Log4j2高性能之—Disruptor队列  

上图显示了当消费者 2 挪动到序号 9 时发生的情况。在这张图中我已经忽略了ConsumerBarrier,因为它没有参与这个场景。

ProducerBarier 会看到下一个节点——序号 3 那个已经可以用了。它会抢占这个节点上的 Entry(我还没有特别介绍 Entry 对象,基本上它是一个放写入到某个序号的 Ring Buffer 数据的桶),把下一个序号(13)更新成 Entry 的序号,然后把 Entry 返回给生产者。生产者可以接着往 Entry 里写入数据。

5.1.4 提交新的数据

两阶段提交的第二步是——对,提交。  Storm、Log4j2高性能之—Disruptor队列  

绿色表示最近写入的 Entry,序号是 13 。

当生产者结束向 Entry 写入数据后,它会要求 ProducerBarrier 提交。

ProducerBarrier 先等待 Ring Buffer 的游标追上当前的位置(对于单生产者这毫无意义-比如,我们已经知道游标到了 12 ,而且没有其他人正在写入 Ring Buffer)。然后 ProducerBarrier 更新 Ring Buffer 的游标到刚才写入的 Entry 序号-在我们这儿是 13。接下来,ProducerBarrier 会让消费者知道 buffer 中有新东西了。它戳一下 ConsumerBarrier 上的 WaitStrategy 对象说-“有事情发生了!”(注意-不同的 WaitStrategy 实现以不同的方式来实现提醒,取决于它是否采用阻塞模式。)

现在消费者 1 可以读 Entry 13 的数据,消费者 2 可以读 Entry 13 以及前面的所有数据,然后它们都过得很 happy。

5.1.5 ProducerBarrier 上的批处理

有趣的是 Disruptor 可以同时在生产者和 消费者 两端实现批处理。还记得伴随着程序运行,消费者 2 最后达到了序号 9 吗?ProducerBarrier 可以在这里做一件很狡猾的事-它知道 Ring Buffer 的大小,也知道最慢的消费者位置。因此它能够发现当前有哪些节点是可用的。

如果 ProducerBarrier 知道 Ring Buffer 的游标指向 12,而最慢的消费者在 9 的位置,它就可以让生产者写入节点 3,4,5,6,7 和 8,中间不需要再次检查消费者的位置。

5.1.6 多个生产者的场景

其实还有一些细节。

ProducerBarrier 拿到的序号直接来自 Ring Buffer 的游标。然而,如果你看过代码的话,你会发现它是通过 ClaimStrategy 获取的。我省略这个对象是为了简化示意图,在单个生产者的情况下它不是很重要。

在多个生产者的场景下,你还需要其他东西来追踪序号。这个序号是指当前可写入的序号。注意这和“向 Ring Buffer 的游标加 1”不一样-如果你有一个以上的生产者同时在向 Ring Buffer 写入,就有可能出现某些 Entry 正在被生产者写入但还没有提交的情况。  Storm、Log4j2高性能之—Disruptor队列  

让我们复习一下如何申请写入节点。每个生产者都向 ClaimStrategy 申请下一个可用的节点。生产者 1 拿到序号 13,这和上面单个生产者的情况一样。生产者 2 拿到序号 14,尽管 Ring Buffer的当前游标仅仅指向 12。这是因为 ClaimSequence 不但负责分发序号,而且负责跟踪哪些序号已经被分配。

现在每个生产者都拥有自己的写入节点和一个崭新的序号。

我把生产者 1 和它的写入节点涂上绿色,把生产者 2 和它的写入节点涂上粉色。  Storm、Log4j2高性能之—Disruptor队列  

现在假设生产者 1 还生活在童话里,因为某些原因没有来得及提交数据。生产者 2 已经准备好提交了,并且向 ProducerBarrier 发出了请求。

就像我们先前在 commit 示意图中看到的一样,ProducerBarrier 只有在 Ring Buffer 游标到达准备提交的节点的前一个节点时它才会提交。在当前情况下,游标必须先到达序号 13 我们才能提交节点 14 的数据。但是我们不能这样做,因为生产者 1 正盯着一些闪闪发光的东西,还没来得及提交。因此 ClaimStrategy 就停在那儿自旋 (spins), 直到 Ring Buffer 游标到达它应该在的位置。 

 

现在生产者 1 从迷糊中清醒过来并且申请提交节点 13 的数据(生产者 1 发出的绿色箭头代表这个请求)。ProducerBarrier 让 ClaimStrategy 先等待 Ring Buffer 的游标到达序号 12,当然现在已经到了。因此 Ring Buffer 移动游标到 13,让 ProducerBarrier 戳一下 WaitStrategy 告诉所有人都知道 Ring Buffer 有更新了。现在 ProducerBarrier 可以完成生产者 2 的请求,让 Ring Buffer 移动游标到 14,并且通知所有人都知道。

你会看到,尽管生产者在不同的时间完成数据写入,但是 Ring Buffer 的内容顺序总是会遵循 nextEntry() 的初始调用顺序。也就是说,如果一个生产者在写入 Ring Buffer 的时候暂停了,只有当它解除暂停后,其他等待中的提交才会立即执行。

5.2 消费者流程

消费者的流程与生产者非常类似,这儿就不多描述了。

5.3生产者的等待策略

暂时只有休眠1ns。

LockSupport.parkNanos(1);


5.4 消费者的等待策略  

名称 措施 适用场景
BlockingWaitStrategy 加锁 CPU资源紧缺,吞吐量和延迟并不重要的场景
BusySpinWaitStrategy 自旋 通过不断重试,减少切换线程导致的系统调用,而降低延迟。推荐在线程绑定到固定的CPU的场景下使用
PhasedBackoffWaitStrategy 自旋 + yield + 自定义策略 CPU资源紧缺,吞吐量和延迟并不重要的场景
SleepingWaitStrategy 自旋 + yield + sleep 性能和CPU资源之间有很好的折中。延迟不均匀
TimeoutBlockingWaitStrategy 加锁,有超时限制 CPU资源紧缺,吞吐量和延迟并不重要的场景
YieldingWaitStrategy 自旋 + yield + 自旋 性能和CPU资源之间有很好的折中。延迟比较均匀

5.5 Log4j2应用效果

Log4j 2相对于Log4j 1最大的优势在于多线程并发场景下性能更优。该特性源自于Log4j 2的异步模式采用了Disruptor来处理。在Log4j 2的配置文件中可以配置WaitStrategy,默认是Timeout策略。下面是Log4j 2中对WaitStrategy的配置官方文档: 

System Property Default Value Description
AsyncLogger.WaitStrategy Timeout Valid values: Block, Timeout, Sleep, Yield. Block is a strategy that uses a lock and condition variable for the I/O thread waiting for log events. Block can be used when throughput and low-latency are not as important as CPU resource. Recommended for resource constrained/virtualised environments. Timeout is a variation of the Block strategy that will periodically wake up from the lock condition await() call. This ensures that if a notification is missed somehow the consumer thread is not stuck but will recover with a small latency delay (default 10ms). Sleep is a strategy that initially spins, then uses a Thread.yield(), and eventually parks for the minimum number of nanos the OS and JVM will allow while the I/O thread is waiting for log events. Sleep is a good compromise between performance and CPU resource. This strategy has very low impact on the application thread, in exchange for some additional latency for actually getting the message logged. Yield is a strategy that uses a Thread.yield() for waiting for log events after an initially spinning. Yield is a good compromise between performance and CPU resource, but may use more CPU than Sleep in order to get the message logged to disk sooner.

性能差异

loggers all async采用的是Disruptor,而Async Appender采用的是ArrayBlockingQueue队列。

由图可见,单线程情况下,loggers all async与Async Appender吞吐量相差不大,但是在64个线程的时候,loggers all async的吞吐量比Async Appender增加了12倍,是Sync模式的68倍。    


图8 Log4j 2各个模式性能比较  

6. 总结

Disruptor相对于传统方式的优点: 

  1. 没有竞争=没有锁=非常快。

  2. 所有访问者都记录自己的序号的实现方式(Sequencer),允许多个生产者与多个消费者共享相同的数据结构。

  3. 在每个对象中都能跟踪序列号(ring buffer,claim Strategy,生产者和消费者),加上神奇的cache line padding和内存屏障对基本类型的强制修饰,就意味着没有为伪共享和非预期的竞争。

  4. 适当的流/批量处理和资源竞争点的分离、合并策略。  吞吐量测试数据(每秒的数量)如下。

环境:- CPU:Intel Core i7 860 @ 2.8 GHz without HT - JVM:Java 1.6.0_25 64-bit - OS:Windows 7  


ABQ Disruptor
Unicast: 1P – 1C 5,339,256 25,998,336
Pipeline: 1P – 3C 2,128,918 16,806,157
Sequencer: 3P – 1C 5,539,531 13,443,268
Multicast: 1P – 3C 1,077,384 9,374,871
Diamond: 1P – 3C 2,113,941 16,163,613

环境:- CPU:Intel Core i7-2720QM - JVM:Java 1.6.0_25 64-bit - OS:Ubuntu 11.04  


ABQ Disruptor
Unicast: 1P – 1C 4,057,453 22,381,778
Pipeline: 1P – 3C 2,006,943 15,797,913
Sequencer: 3P – 1C 2,054,118 14,540,519
Multicast: 1P – 3C 260,433 10,860,121
Diamond: 1P – 3C 2,082,124 15,235,197

依据并发竞争的激烈程度的不同,Disruptor比ArrayBlockingQueue吞吐量快4~8倍。

按照Pipeline: 1P – 3C的连接模式测试延迟,生产者两次写入之间的延迟为1ms。

Thanks

表格及图片引用:

  1. http://brokendreams.iteye.com/blog/2255720

  2. http://ifeve.com/dissecting-disruptor-whats-so-special/

  3. https://github.com/LMAX-Exchange/disruptor/wiki/Performance-Results

  4. https://lmax-exchange.github.io/disruptor/

  5. https://logging.apache.org/log4j/2.x/manual/async.html

  6. https://tech.meituan.com/2016/11/18/disruptor.html