Disruptor
Disruptor
在了解Java交易撮合引擎时看到了Disruptor,印象中美团好像写过相关的文章,所以赶紧来学习一下,顺便重温一下计算机缓存相关的知识。
缓存行
Java:对于多维数组,Java实际上创建的是“数组的数组”。例如,一个二维数组其实是一个包含多个一维数组的数组。尽管每个一维数组内部的元素是连续存储的,但这些一维数组本身可能不是连续存储的。所以通过列遍历时,内存访问模式并不是连续的,无法利用cache line的特性,这会降低读取性能。
在多线程模式下,可能会出现伪共享的情况,即多个线程同时访问同一块内存,但是由于内存的布局原因,可能会导致一个线程读取到另一个线程所修改的数据。这种情况下,CPU会频繁地进行缓存一致性协议,导致性能下降。比如:某线程一次读取cache line的数据,修改了其中的一个数据,然后写回cache line,此时另一个线程读取到该cache line,由于数据被修改,因此需要重新从主存中读取数据,从而导致性能下降。
Java8+中提供了比较优雅的解决方案:
import sun.misc.Contended;
@Contended
public final static class ValuePadding {
protected volatile long value = 0L;
}
Disruptor巧妙的设计
通过以下设计来解决队列速度慢的问题:
- 环形数组结构
为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好。
为什么数组比链表更友好?
- 内存连续性 & 缓存友好性
数组:
内存连续分配,CPU 缓存预取(Prefetching)更高效。
访问 array[i] 时,相邻元素(如 array[i+1])可能已经在缓存行(Cache Line)中,减少 缓存未命中(Cache Miss)。
适合顺序访问(如遍历、批量计算)。
链表:
节点在内存中 非连续存储,每次访问 node.next 可能触发 缓存未命中。
每个 Node 对象额外存储 next 指针,占用更多内存,缓存利用率低。
- 垃圾回收(GC)开销
数组:
单块连续内存,GC 扫描更快(只需标记整个数组,而不是每个元素)。
如果存储基本类型(如 int[]、long[]),无对象头开销,减少内存占用。
链表:
每个 Node 是独立对象,GC 需要遍历所有节点,增加停顿时间(Stop-The-World)。
频繁增删节点会导致 内存碎片,进一步影响 GC 效率。
- 元素位置定位
数组长度2^n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。
- 无锁设计
每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。