Java SDK 提供了 2 个有界队列:ArrayBlockingQueue 和 LinkedBlockingQueue,它们都是基于 ReentrantLock 实现的,在高并发场景下,锁的效率并不高。
今天我们就介绍一种性能更高的有界队列:Disruptor。
Disruptor 是一款高性能的有界内存队列,Disruptor 性能之高,取决于下面四点,下面我们分别来讲解。
1.内存分配更加合理,使用 RingBuffer 数据结构,数组元素在初始化时一次性全部创建,提升缓存命中率;对象循环利用,避免频繁 GC.
该做法利用局部性原理,时间局部性,空间局部性,cpu缓存就利用了程序局部性原理,cpu从内存中加载数据x时,会将数据x缓存到高速缓存cache中(缓存行),同时还缓存了x周围的数据(连续地址)。这样在访问周围数据时,不用再从内存中填到缓存读取。
ArrayBlockingQueue底层也是采用数组的形式存放元素,E1,E2等元素都是由生产者创建的,创建这些元素的时间基本上都是离散的,所以这些元素的内存地址大概率是不连续了。
这里你可能会问,数组的内存地址不是连续的么?数组是连续的,在里面存放的引用,E1,E2元素元素对象地址不连续。
而Disruptor中的RingBuffer数组中的元素在初始化时是一次性全部创建的,所以这些元素的内存地址大概率是连续的,生产者每publishEvent发布Event时,不会创建Event,而是Event.set修改初始化的Event.也就是说 RingBuffer 创建的 Event 是可以循环利用的,这样还能避免频繁创建、删除 Event 导致的频繁 GC 问题。
2. 能够避免伪共享,提升缓存利用率。
伪共享就是多个cup中缓存同一个缓存行,该缓存行中有多个元素,如果修改一个元素,会使其他cpu缓存了该缓存行无效,使得其他线程访问其他元素不成功,没有很好利用cache.
比如ArrayBlockingQueue当cpu从内存中加载takeindex,会同时将putindex加载进缓存行中,当A入队操作,修改takeindex,会把缓存行写入到内存中,导致其他cpu上的缓存行失效,如果B出队,正要修改呢,缓存行失效,就得重新从内存中加载。造成缓存利用率不高。
但是ArrayBlockingQueue内部使用锁,不可能出现出队入队同时出现的情况。
使用缓存行填充避免伪共享
3. 采用无锁算法,避免频繁加锁、解锁的性能消耗。
对于入队操作,最关键的要求是不能覆盖没有消费的元素;对于出队操作,最关键的要求是不能读取没有写入的元素。
Disruptor使用索引来入队和出队,比如入队,如果没有足够的空余位置,就出让 CPU 使用权,然后重新计算;反之则用 CAS 设置入队索引。
//生产者获取n个写入位置
do {
//cursor类似于入队索引,指的是上次生产到这里
current = cursor.get();
//目标是在生产n个
next = current + n;
//减掉一个循环
long wrapPoint = next - bufferSize;
//获取上一次的最小消费位置
long cachedGatingSequence = gatingSequenceCache.get();
//没有足够的空余位置
if (wrapPoint>cachedGatingSequence || cachedGatingSequence>current){
//重新计算所有消费者里面的最小值位置
long gatingSequence = Util.getMinimumSequence(
gatingSequences, current);
//仍然没有足够的空余位置,出让CPU使用权,重新执行下一循环
if (wrapPoint > gatingSequence){
LockSupport.parkNanos(1);
continue;
}
//从新设置上一次的最小消费位置
gatingSequenceCache.set(gatingSequence);
} else if (cursor.compareAndSet(current, next)){
//获取写入位置成功,跳出循环
break;
}
} while (true);
4. 支持批量消费,消费者可以无锁方式消费多个消息。