Disruptor是什么
Disruptor是一个由英国外汇交易公司LMAX开源的Java高性能队列,它能够以很低的延迟产生大量交易,能够在一个线程里每秒处理 6 百万订单,可以认为它是线程间通信高效低延时的内存消息组件(简单的看其实就是一个有界队列,类似于ArrayBlockingQueue,我们知道 BlockingQueue 是一个 FIFO 队列,生产者Producer往队列里发布publish一项事件时,消费者Consumer能获得通知;如果没有事件时,消费者被堵塞,直到生产者发布了新的事件)。
传统队列
java提供的队列
我们先看看java常用的队列:
常用的java队列
常用的主要分两大类:基于数组线程安全的队列,比较典型的是ArrayBlockingQueue,它主要通过加锁的方式来保证线程安全;基于链表的线程安全队列分成LinkedBlockingQueue和ConcurrentLinkedQueue两大类,前者也通过锁的方式来实现线程安全,而后者以及上面表格中的LinkedTransferQueue都是通过原子变量compare and swap这种不加锁的方式来实现的。
通过不加锁的方式实现的队列都是无界的(无法保证队列的长度在确定的范围内);而加锁的方式,可以实现有界队列。在稳定性要求特别高的系统中,为了防止生产者速度过快,导致内存溢出,只能选择有界队列;同时,为了减少Java的垃圾回收对系统性能的影响,会尽量选择array/heap格式的数据结构(地址是连续的)。平时业务中用得最多的是ArrayBlockingQueue;
加锁
上面说了需要加锁来保证线程并发安全,加锁通常会影响性能。线程会因为竞争不到锁而被挂起,等锁被释放的时候,线程又会被恢复,这个过程中存在着很大的开销,并且通常会有较长时间的中断,因为当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下发生了缺页错误、调度延迟或者其它类似情况,那么所有需要这个锁的线程都无法执行下去。另外还可能发生死锁的情况。
官方给出了一个实验:
- 调用了一个函数,该函数会对一个64位的计数器循环自增5亿次
- 机器环境:2.4G 6核
- 运算: 64位的计数器累加5亿次
性能对比
官方对比
下面官方给出了和ArrayBlockingQueue的比较结果
https://github.com/LMAX-Exchange/disruptor/wiki/Performance-Results
Log4j 2应用场景
Log4j 2相对于Log4j 1最大的优势在于多线程并发场景下性能更优,loggers all async采用的是Disruptor,而Async Appender采用的是ArrayBlockingQueue队列。
Log4j 2各个模式性能比较
Disruptor为什么快
缓存行和伪共享
我们知道为了提高访问速度,CPU和主内存之间是有好几层缓存,越靠近CPU的缓存越快也越小
数据在缓存中不是以独立的项来存储的,缓存是由缓存行组成的,通常是64字节,并且它有效地引用主内存中的一块地址。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。这就会存在一个问题,当你去除一个缓存行的时候(cpu规定必须以整个缓存行作为单位来处理),如果你拿到了其他线程的不相关数据,并且更新了你自己的数据,缓存中的值和内存中的值都被更新了,而其他所有存储当前缓存行都会都会失效,那么一个和你的消费者无关的线程读一个和它无关的值,它被缓存未命中给拖慢了。这就是所谓的伪共享。
image
Disruptor如何设计
-
缓存行填充
因此没有伪共享,就没有和其它任何变量的意外冲突,没有不必要的缓存未命中。 -
环形数组结构RingBuffer
RingBuffer是一个内存环,每一次读写操作都循环利用这个内存环,从而避免频繁分配和回收内存,减轻GC压力,同时由于RingBuffer可以实现为无锁的队列,从而整体上大幅提高系统性能。RingBuffer是由一个大数组组成的。RingBuffer没有尾指针,维护了一个指向下一个可用位置的序号。RingBuffer和常用的队列之间的区别是,不删除buffer中的数据,也就是说这些数据一直存放在buffer中,直到新的数据覆盖他们。(比链表快,对CPU缓存友好,在硬件级别,数组中的元素是会被预加载的,因此在ringbuffer当中,cpu无需时不时去主存加载数组中的下一个元素)
image -
元素位置定位
数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。 -
无锁设计
每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。
消费数据
image消费者(Consumer)是一个想从Ring Buffer里读取数据的线程,它可以访问ConsumerBarrier对象——这个对象由RingBuffer创建并且代表消费者与RingBuffer进行交互,当消费者处理完了Ring Buffer里序号8之前(包括8)的所有数据,那么它期待访问的下一个序号是9。接下来,消费者会一直原地停留,等待更多数据被写入Ring Buffer。并且,一旦数据写入后消费者会收到通知——节点9,10,11和12 已写入。现在序号12到了,消费者可以让ConsumerBarrier去拿这些序号节点里的数据了。
生产数据
image生产者这边有个consumerTrackingProducerBarrier 对象拥有所有正在访问 Ring Buffer 的消费者列表,维护了当前所有访问的消费者所处的位置。现在生产者想要写入 Ring Buffer 中序号 3 占据的节点,因为它是 Ring Buffer 当前游标的下一个节点。但是 ProducerBarrier 明白现在不能写入,因为有一个消费者正在占用它。所以,ProducerBarrier 停下来自旋 等待,直到那个消费者离开。
image
提交新的数据,当生产者结束向 Entry 写入数据后,它会要求 ProducerBarrier 提交。如果没有冲突提交成功后,则会通知ConsumerBarrier 上的 WaitStrategy,有新的数据产生,这时候消费者就可以消费的新的数据了。
网友评论