1. 数据结构-内存预加载机制
数据结构层面:使用环形结构、数组、内存预加载
RingBuffer使用数组Object[] entries作为存储元素,如下图所示
2. 内核-使用单线程写
Disruptor的RingBuffer,之所以可以做到完全无锁,是因为"单线程写"这个前提。
离开了这个前提,没有任何技术可以做到安全无锁
Redis、Netty等高性能技术框架的设计都是这个核心思想
3.系统内存优化-内存屏障
- 要正确的实现无锁,还需要另外一个关键技术:内存屏障。
- 对应到Java语言,就是valotile变量与happens before语义。
- 内存屏障 - Linux的smp_wmb()/smp_rmb()
4.系统缓存优化-消除伪共享
4.1缓存行与伪共享
缓存中的数据并不是独立的进行存储的,它的最小存储单位是缓存行,缓存行的大小是2的整数幂个字节,最常见的缓存行大小是 64 字节。CPU 为了执行的高效,会在读取某个对象时,从内存上加载 64 的整数倍的长度,来补齐缓存行。
以 Java 的 long 类型为例,它是 8 个字节,假设我们存在一个长度为 8 的 long 数组 arr,那么CPU 在读取 arr[0] 时,首先查询缓存,缓存没有命中,缓存就会去内存中加载。由于缓存的最小存储单位是缓存行,64 字节,且数组的内存地址是连续的,则将 arr[0] 到 arr[7] 加载到缓存中。后续 CPU 查询 arr[6] 时候也可以直接命中缓存。
现在假设多线程情况下,线程 A 的执行者 CPU Core-1 读取 arr[1],首先查询缓存,缓存没有命中,缓存就会去内存中加载。从内存中读取 arr[1] 起的连续的 64 个字节地址到缓存中,组成缓存行。由于从arr[1] 起,arr 的长度不足够 64 个字节,只够 56 个字节。假设最后 8 个字节内存地址上存储的是对象 bar,那么对象 bar 也会被一起加载到缓存行中。
现在有另一个线程 B,线程 B 的执行者 CPU Core-2 去读取对象 bar,首先查询缓存,发现命中了,因为 Core-1 在读取 arr 数组的时候也顺带着把 bar 加载到了缓存中。
这就是缓存行共享,听起来不错,但是一旦牵扯到了写入操作就不妙了。
假设 Core-1 想要更新 arr[7] 的值,根据 CPU 的 MESI 协议,那么它所属的缓存行就会被标记为失效。因为它需要告诉其他的 Core,这个 arr[7] 的值已经被更新了,缓存已经不再准确了,你必须得重新去内存拉取。但是由于缓存的最小单元是缓存行,因此只能把 arr[7] 所在的一整行给标识为失效。
此时 Core-2 就会很郁闷了,刚刚还能够从缓存中读取到对象 bar,现在再读取却被告知缓存行失效,必须得去内存重新拉取,延缓了 Core-2 的执行效率。
这就是缓存伪共享问题,两个毫无关联的线程执行,一个线程却因为另一个线程的操作,导致缓存失效。这两个线程其实就是对同一缓存行产生了竞争,降低了并发性。
4.2 Disruptor 为了解决伪共享
- 缓存系统中是以缓存行(cache line)为单位存储的
- 缓存行是以2的整数幂个连接字节,一般为32~256个字节
- 最常见的缓存行大小是64个字节
- 当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行
- 就会无意中影响彼此的性能,这就是伪共享
Disruptor 为了解决伪共享问题,使用的方法是缓存行填充。这是一种以空间换时间的策略,主要思想就是通过往对象中填充无意义的变量,来保证整个对象独占缓存行。
举个例子,以 Disruptor 中的 Sequence 为例,在 volatile long value 的前后各放置了 7 个 long 型变量,确保 value 独占一个缓存行。
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;
}
public class Sequence extends RhsPadding
{
...
}
如下图所示,其中 V 就是 Value 类的 value,P 为 value 前后填充的无意义 long 型变量,U 为其它无关的变量。不论什么情况下,都能保证 V 不和其他无关的变量处于同一缓存行中,这样 V 就不会被其他无关的变量所影响。
这里的 V 也不限定为 long 类型,其实只要对象的大小大于等于8个字节,通过前后各填充 7 个 long 型变量,就一定能够保证独占缓存行。
此处以 Disruptor 的 RingBuffer 为例,最左边的 7 个 long 型变量被定义在顶级父类 RingBufferPad 中,最右边的 7 个 long 型变量被定义在 RingBuffer 的最后一行变量定义中,这样所有的需要独占的变量都被左右 long 型给包围,确保会独占缓存行。
public final class RingBuffer extends RingBufferFields implements Cursored, EventSequencer, EventSink {
public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE;
protected long p1, p2, p3, p4, p5, p6, p7;
...
}
abstract class RingBufferFields extends RingBufferPad
{
...
}
abstract class RingBufferPad {
protected long p1, p2, p3, p4, p5, p6, p7;
}
5.算法优化-序号栅栏机制
- 我们在生产者进行投递Event的时候,总会使用:long sequence = ringBuffer.next();
Disruptor 3.0中,序号栅栏SequenceBarrier和序号Sequence搭配使用,协同和2. 管理消费者与生产者的工作节奏,避免了锁和CAS的使用 - 在Disruptor3.0中,各个消费者和生产者持有自己的序号,这些序号的变化必须满足如下基本条件:
-- 参见源码:SingleProducerSequencer
public long next(int n)
{
if (n < 1)
{
throw new IllegalArgumentException("n must be > 0");
}
long nextValue = this.nextValue;
long nextSequence = nextValue + n;
long wrapPoint = nextSequence - bufferSize;
long cachedGatingSequence = this.cachedValue;
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
{
cursor.setVolatile(nextValue); // StoreLoad fence
long minSequence;
//Util.getMinimumSequence(gatingSequences, nextValue) 含义就是找到消费者中 最小的序号值
//如果 你的生产者序号大于消费者中已消费最小的序号 那么就挂起进行自旋操作
while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
{
LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
}
this.cachedValue = minSequence;
}
this.nextValue = nextSequence;
return nextSequence;
}
a. 消费者的序号数值必须小于生产者序号数值;
b. 消费者序号数值必须小于其前置(依赖关系)消费者的序号数值;
c. 生产者序号数值不能大于消费者最小的序号数值以避免生产者速度过快,将还未来得及消费的消息覆盖
6.WaitStrategy等待策略
Disruptor之所以说是高性能,其实也有一部分原因取决于他等待策略的实现:
WaitStrategy
EventProcessor核心机制深度分析
EventHandler深度分析
特别感谢:
阿神