目录
一、介绍
Disruptor是一个开源的并发框架,它被设计用于在生产者—消费者(Producer-Consumer Problem,简称PCP)问题上获得高吞吐量(TPS)和低延迟。为了解决高并发下列队锁的问题,最早由LMAX(一种新型零售金融交易平台)提出并使用,能够在无锁的情况下实现队列的并发操作,并号称能够在一个线程里每秒处理6百万笔订单。
PCP又称Bounded-Buffer问题,其核心就是保证对一个Buffer的存取操作在多线程环境下不会出错。使用Java中的ArrayBlockingQueue和LinkedBlockingQueue类能轻松的完成PCP模型,这对于一般程序已经没问题了,但是对于并发度高、TPS要求较大的系统则不然。
二、为什么速度更快?
1. JDK中队列加锁
如下图所示,是JDK自带的队列其特点对比和总结:
*BlockingQueue使用的是package java.util.concurrent.locks中实现的锁,当多个线程(例如多生产者)同时写入Queue时,锁的争抢会导致只有一个生产者可以执行,其他线程都中断,也就是线程的状态从RUNNING切换到BLOCKED,直到某个生产者线程使用完Buffer后释放锁,其他线程状态才从BLOCKED切换到RUNNABLE,然后时间片到其他线程后再进行锁的争抢。
上述过程中,一般来说生产者存放一个数据到Buffer中所需时间是非常短,操作系统切换线程上下文的速度也是非常快,但是当线程数量增多后,OS切换线程所带来的开销逐渐增多,锁的反复申请和释放成为性能瓶颈。
*BlockingQueue除了使用锁带来的性能损失外,还可能因为线程争抢的顺序问题造成性能再次损失:实际使用中线程的调度顺序并不理想,可能出现短时间内OS频繁调度生产者或消费者的情况,这样造成缓冲区可能短时间内被填满或被清空的极端情况(理想情况应该是缓冲区长度适中,生产和消费速度基本一致)。
2. Disruptor无锁机制
Disruptor采用无锁机制。使用一个Ring Buffer的环形缓冲区数组(一段连续内存)。称为环形是因为它对数据存放位置的处理,生产者和消费者各有一个指针(数组下标),消费者的指针指向下一个要读取的Slot,生产者指针指向下一个要放入的Slot,消费或生产后,各自的指针值p = (p +1) % n,n是缓冲区长度(决定了缓存长度必须是2的次幂),这样指针在缓冲区上反复游走,故可以将缓冲区看成环状。如下图所示。
Ring Buffer使用情况:
单生产者和单消费者:两个线程分别操作不同的指针,不需要锁。
多个消费者:每个消费者各自控制自己的指针,依次读取每个Slot(每个消费者读取到所有产品),只需要保证生产者指针不会超过最慢的消费者即可,不需要锁。
多个生产者:多个线程共用一个写指针,使用CAS来保证多线程安全。
三、Disruptor特点
1. 无锁
详见第二节的Disruptor无锁机制。
2. 预分配
预分配是一个空间换时间的思想。创建Disruptor时,EventFactory创建事件实例填充整个RingBuffer,而不是每次生产者生产事件时去创建事件对象,只需要事件对象属性值赋值给这些实例即可。这样可以避免JVM大量创建和回收对象对GC造成压力。
3. 缓存行填充
CPU与内存的速度相差很大,而每个指令周期中的读/写指令都要依赖内存。为了解决这个差距,加入缓存层,即CPU高速缓存(与CPU速度对等的是寄存器)。
CPU缓存以64bytes大小作为一个缓存行,缓存由若干个缓存行组成,缓存写回主存或主存写入缓存均是以行为单位,此外每个CPU核心都有自己的缓存(若某个核对某缓存行做出修改,其他拥有同样缓存的核需要进行同步)。生产者和消费者的指针用long型表示(8bytes),如果生产者和消费者的指针(加起来共16bytes)出现在同一个缓存行中会怎么样?
缓存行填充(Cache Line Padding):对于一个long型的缓冲区指针,用一个长度为8的long型数组代替(即:一个指针存储在一个缓存行中)。一个缓存行被这个数组填充满,线程对各自指针的修改不会干扰到其他CPU,从而解决CPU伪共享问题。
4. 批操作
Ring Buffer的核心操作是生产和消费,如果能减少这两个操作的次数,性能也相应提高。
RingBuffer两个阶段生产:
阶段一:申请空间,申请后生产者获得了一个指针范围[low,high],然后再对缓冲区中[low,high]这段的所有事件对象进行setValue;
阶段二:ringBuffer.publish(low,high)批量发布。
阶段一结束后,其他生产者再申请的话,会得到其后一段缓冲区。阶段二结束后,之前申请的这一段数据就可以被消费者读到。Disruptor推荐成批生产、成批发布。
四、核心组件
- RingBuffer:环形队列(Disruptor的核心),长度必须是2^N。Disruptor启动时,事件工厂创建事件示例预先填充该队列,生产者的属性值赋值给事件示例;
- EventFactory:事件工厂,创建事件示例预先填充RingBuffer队列;
- Sequencer:序号管理器,根据Sequence使消费者和生产者之前快速正确地传输数据;
- Sequence:事件示例在RingBuffer的位置,跟踪Ringbuffer中任务的变化和消费者的消费情况;
- WorkerPool:存储WorkProcessor的池子,Disruptor将任务放入池子,Executor并行启动每一个WorkProcessor;
- WorkProcessor:从RingBuffer消费事件,并交给WorkHandler处理任务;
- WorkHandler:处理任务的工作者,根据任务类型交给不同的EventHandler处理;
- EventHandler:消费者实现类,获取事件示例进行业务处理;
- ExceptionHandler:所有消费者业务处理中报错的统一异常处理;
- EventTranslator:将具体事件的属性值赋值给RingBuffer队列中示例,并发送RingBuffer中;
- Disruptor:创建并启动Disruptor,定义事件工厂、RingBuffer队列大小、线程池处理、生产者类型(单生产者、多生产者)、队列等待策略、消费者依赖关系等。
五、参考资料
无锁并发框架-Disruptor的原理(一)【图文】_zhz小白弟弟_51CTO博客
Disruptor3.0的实现细节 - 倒骑的驴 - 博客园