
构建高性能内存队列:Disruptor yyds~
Java中有哪些队列
-
ArrayBlockingQueue
使用ReentrantLock -
LinkedBlockingQueue
使用ReentrantLock -
ConcurrentLinkedQueue
使用CAS - 等等
我们清楚使用锁的性能比较低,尽量使用无锁设计。接下来就我们来认识下Disruptor。
Disruptor简单使用
github地址:https://github.com/LMAX-Exchange/disruptor/wiki/Performance-Results
先简单介绍下:
- Disruptor它是一个开源的并发框架,并获得2011 Duke’s程序框架创新奖【Oracle】,能够在无锁的情况下实现网络的Queue并发操作。英国外汇交易公司LMAX开发的一个高性能队列,号称单线程能支撑每秒600万订单~
- 日志框架Log4j2 异步模式采用了Disruptor来处理
- 局限呢,他就是个内存队列,也就是说无法支撑分布式场景。
简单使用
数据传输对象
消费者
生产者
测试类
核心组件
基于上面简单例子来看确实很简单,Disruptor帮我们封装好了生产消费模型的实现,接下来我们来看下他是基于哪些核心组件来支撑起一个高性能无锁队列呢?
RingBuffer: 环形数组,底层使用数组entries,在初始化时填充数组,避免不断新建对象带来的开销。后续只会对entries做更新操作
Sequencer: 核心管家
- 定义生产同步的实现:
SingleProducerSequencer
单生产、MultiProducerSequencer
多生产 - 当前写的进度Sequence cursor
- 所有消费者进度的数组
Sequence[] gatingSequences
-
MultiProducerSequencer
可用区availableBuffer
【利用空间换取查询效率】
Sequence: 本身就是一个序号器用来标识处理进度,也可以当做是一个atomicInteger
; 还有另外一个特点,为了解决伪共享问题而引入的:缓存行填充。这个在后面介绍。
workProcessor: 处理Event的循环,在循环中获取Disruptor的事件,然后把事件分配给各个handler
EventHandler: 负责业务逻辑的handler,自己实现。
WaitStrategy: 消费者 如何等待 事件的策略,定义了如下策略
-
leepingWaitStrategy
:自旋 + yield + sleep -
BlockingWaitStrategy
:加锁,适合CPU资源紧张(不需要切换线程),系统吞吐量无要求的 -
YieldingWaitStrategy
:自旋 + yield + 自旋 -
BusySpinWaitStrategy
:自旋,减少线程之前切换 -
PhasedBackoffWaitStrategy
:自旋 + yield + 自定义策略
带着问题来解析代码?
1、多生产者如何保证消息生产不会相互覆盖。【如何达到互斥效果】
每个线程获取不同的一段数组空间,然后通过CAS判断这段空间是否已经分配出去。
接下来我们看下多生产类MultiProducerSequencer
中next方法【获取生产序号】
2、生产者向序号器申请写的序号,如序号正在被消费,Sequencer是如何知道哪些序号是可以被写入的呢?【未消费则被覆盖如何处理】
从gatingSequences中取得最小的序号,生产者最多能写到这个序号的后一位。通俗来讲就是申请的序号不能大于最小消费者序号一圈【申请到最大序列号-buffersize 要小于/等于 最小消费的序列号】的时候, 才能申请到当前写的序号
3、在多生产者情况下,生产者是申请到一段可写入的序号,然后再写入这些序号中,那么消费者是如何感知哪些序号是可以被消费的呢?【借问提1图说明】
这个前提是多生产者情况下,第一点我们说过每个线程获取不同的一段数组空间,那么现在单单通过序号已经不够用了,MultiProducerSequencer
使用了int 数组 【availableBuffer
】来标识当前序号是否可用。当生产者成功生产事件后会将availableBuffer
中当前序列号置为1标识可以读取。
如此消费者可以读取的的最大序号就是我们availableBuffer
中第一个不可用序号-1。
初始化availableBuffer
流程
消费者消费流程
解决伪共享问题
什么是伪共享问题呢?
为了提高CPU的速度,Cpu有高速缓存Cache,该缓存最小单位为缓存行CacheLine,他是从主内存复制的Cache的最小单位,通常是64字节。一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量。如果你访问一个long数组,当数组中的一个值被加载到缓存中,它会额外加载另外7个。因此你能非常快地遍历这个数组。
伪共享问题是指,当多个线程共享某份数据时,线程1可能拉到线程2的数据在其cache line中,此时线程1修改数据,线程2取其数据时就要重新从内存中拉取,两个线程互相影响,导致数据虽然在cache line中,每次却要去内存中拉取。
Disruptor是如何解决的呢?
在value前后统一都加入7个Long类型进行填充,线程拉取时,不论如何都会占满整个缓存
回顾总结:Disuptor为何能称之为高性能的无锁队列框架呢?
- 缓存行填充,避免缓存频繁失效。【java8中也引入
@sun.misc.Contended
注解来避免伪共享】 - 无锁竞争:通过CAS 【二阶段提交】
- 环形数组:数据都是覆盖,避免GC
- 底层更多的使用位运算来提升效率
本文转载自公众号:码猿技术专栏
