无锁并发框架---Disruptor
1、Disruptor框架简介
1.1、Disruptor是干什么的
Disruptor是一个开源框架,研发的初衷是为了解决高并发下队列锁的问题,最早由 LMAX(一种新型零售金融交易平台)提出并使用,能够在无锁的情况下实现队列的并发操 作,并号称能够在一个线程里每秒处理6百万笔订单。这个吞吐量那是相当高了。
这个框架最经典的应用场景:生产者、消费者。
1.2、核心设计原理
Disruptor通过以下设计来解决队列速度慢的问题:
1、环形数组结构:
为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好(由于CPU加载空间局部性原则)。类似于下面这种(这只是一种画出来我们看的比较舒服的模式而已):
2、元素位置定位:
数组长度2n,通过位运算,加快定位的速度。下标采取递增的形式。不用担心index溢出的问 题。index是long类型,即使100万QPS的处理速度,也需要30万年才能用完。
3、无锁设计:
每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。
1.3、底层数据结构
框架使用RingBuffer来作为队列的数据结构,RingBuffer就是一个可自定义大小的环形数组。除数组外还有一个序列号(sequence),用以指向下一个可用的元素,供生产者与消费者使用,意思就是第一轮是0-11,第二轮下标就是12-23,第三轮下标就是24-35,后续以此类推,这里面维护这个序号的是一个叫做equence的存储结构,如下图所示:
1.4、Disruptor设计成数组+序列号的优势是什么?
1、数组的访问速度是O(1)。
2、获取下标时可以通过序列号与数组长度取模或者通过 & 运算来得出,比较方便。
3、数组是环形的,重复使用,也避免了堆的GC。
1.5、Disruptor内部的一些构件简介
1、RingBuffer:Disruptor底层数据结构实现,核心类,是线程间交换数据的中转地。
2、Sequencer:序号管理器,生产同步的实现者,负责消费者/生产者各自序 号、序号栅栏的管理和协调,Sequencer有单生产者,多生产者两种不同的模式,里面实现了各种同步的算法。
3、Sequence:序号,声明一个序号,用于跟踪ringbuffer中任务的变化和消费者的消费情况,disruptor里面大部分的并发代码都是通过对Sequence的值同步修改实现的,而非锁,这是disruptor高性能的一个主要原因。
4、SequenceBarrier:序号栅栏,管理和协调生产者的游标序号和各个消费者的 序号,确保生产者不会覆盖消费者未来得及处理的消息,确保存在依赖的消费者之间能够按照正确的顺序处理, Sequence Barrier是由Sequencer创建的,并被 Processor持有。
5、EventProcessor:事件处理器,监听RingBuffer的事件,并消费可用事件, 从RingBuffer读取的事件会交由实际的生产者实现类来消费;它会一直监听下一个可用的序号,直到该序号对应的事件已经准备好。
5、EventHandler:业务处理器,是实际消费者的接口,完成具体的业务逻辑实 现,第三方实现该接口;代表着消费者。
6、Producer:生产者接口,第三方线程充当该角色,producer向RingBuffer写 入事件。
7、Wait Strategy:Wait Strategy决定了一个消费者怎么等待生产者将事件 (Event)放入Disruptor中。
1.6、Disruptor的等待策略
1、BlockingWaitStrategy:Disruptor的默认策略是BlockingWaitStrategy。在BlockingWaitStrategy内部是使用锁和condition来控制线程的唤醒。BlockingWaitStrategy是最低效的策略,但其对CPU 的消耗最小并且在各种不同部署环境中能提供更加一致的性能表现。
2、SleepingWaitStrategy:SleepingWaitStrategy 的性能表现跟 BlockingWaitStrategy 差不多,对 CPU 的消耗也类似,但其对生产者线程的影响最小,通过使用LockSupport.parkNanos(1)来实现循环等待。一般来说Linux系统会暂停一个线程约60µs,这样做的好处是,生产线程不需要采取任何其他行动就可以增加适当的计数器,也不需要花费时间信号通知条件变量。但是,在生产者线程和使用者线程之间移动事件的平均延迟会更高。它在不需要低延迟并且对生产线程的影响较小的情况最好。一个常见的用例是异步日志记录。
3、YieldingWaitStrategy:YieldingWaitStrategy是可以使用在低延迟系统的策略之一。YieldingWaitStrategy 将自旋以等待序列增加到适当的值。在循环体内,将调用Thread.yield(),以允许其他排队的线程运行。在要求极高性能且事件处理线数小于 CPU 逻辑核心数的场景中,推荐使用 此策略;例如,CPU开启超线程的特性。
4、BusySpinWaitStrategy:性能最好,适合用于低延迟的系统。在要求极高性能且事件处理线程数小于CPU逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。
2、Disruptor框架的使用
1、pom文件加入下面依赖
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.2.1</version>
</dependency>
2、定义一个事件,就是环形数组里面存放的事件,由 LongEventFactory 来创建
/**
* 定义一个事件,就是环形数组里面存放的事件,由 LongEventFactory 来创建
* @author dujlc
*/
public class LongEvent {
private Long value;
public Long getValue() {
return value;
}
public void setValue(Long value) {
this.value = value;
}
}
3、创建任务的工厂类
/**
* 这个类取创建任务,实现 EventFactory 接口,它就会自动把创建的对象添加到 环形任务队列中
* 返回一个 LongEvent 的对象即可,相当于一个创建任务的工厂
* @author dujlc
*/
public class LongEventFactory implements EventFactory<LongEvent> {
@Override
public LongEvent newInstance() {
return new LongEvent();
}
}
4、消费者具体的类
/**
* 实现 EventHandler 接口,即可定义一个消费者,
* Disruptor框架会自己调用 onEvent 方法执行消费者的代码
* @author dujlc
*/
//@Slf4j
public class LongEventHandler implements EventHandler<LongEvent> {
@Override
public void onEvent(LongEvent event, long sequence, boolean endOfBatch) throws Exception {
System.out.println("消费者:" + event.getValue());
}
}
5、生产者具体操作的类
/**
* 定义一个生产者
* @author dujlc
*/
//@Slf4j
public class LongEventProducer {
public final RingBuffer<LongEvent> ringBuffer;
public LongEventProducer(RingBuffer<LongEvent> ringBuffer) {
this.ringBuffer = ringBuffer;
}
public void onData(ByteBuffer byteBuffer) {
// 1.ringBuffer 事件队列下一个槽,看是否合法,是否可以写入数据
// 如果成功,则会返回一个序列号,如果失败,则会阻塞在这里
long sequence = ringBuffer.next();
Long data = null;
try {
//2.取出空的事件队列
// 也就是取出那个槽位的使用权
LongEvent longEvent = ringBuffer.get(sequence);
data = byteBuffer.getLong(0);
//3.获取事件队列传递的数据
// 将具体的数据给写入进去
longEvent.setValue(data);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} finally {
System.out.println("生产者准备发送数据...");
//4.发布事件
ringBuffer.publish(sequence);
}
}
}
6、主类
public class DisruptorMain {
public static void main(String[] args) {
// 1.创建一个可缓存的线程 提供线程来出发Consumer 的事件处理
ExecutorService executor = Executors.newCachedThreadPool();
// 2.创建工厂
EventFactory<LongEvent> eventFactory = new LongEventFactory();
// 3.创建 ringBuffer 大小,ringBufferSize大小一定要是2的N次方
int ringBufferSize = 1024 * 1024;
// 4.创建Disruptor
// eventFactory 创建事件的工厂
// ringBufferSize 环形数组的大小
// executor 消费者线程池
// ProducerType.SINGLE 默认单例生产者
// new YieldingWaitStrategy() 等待策略,让出CPU使用权的那种
Disruptor<LongEvent> disruptor = new Disruptor<LongEvent>(eventFactory, ringBufferSize, executor,
ProducerType.SINGLE, new YieldingWaitStrategy());
// 5.连接消费端方法
// 具体的消费代码是哪个类,也就是定义消费者的那个类
disruptor.handleEventsWith(new LongEventHandler(), new LongEventHandler());
// 6.启动
disruptor.start();
// 7.取出创建好的RingBuffer容器
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
// 8.创建生产者,生产者需要自己创建并指定生产次数
LongEventProducer producer = new LongEventProducer(ringBuffer);
// 9.指定缓冲区大小,最好指定2^n
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
// 指定生产100次
for (int i = 1; i <= 100; i++) {
byteBuffer.putLong(0, i);
// 调用 onData 方法去生产
producer.onData(byteBuffer);
}
//10.关闭disruptor和executor
disruptor.shutdown();
executor.shutdown();
}
}
7、运行截图如下:
3、Disruptor框架的重点方法解析
3.1、ByteBuffer.next()
生产者在生产数据之前会用到这个方法。
这个方法只要是判断后续的几个坑位是否还可以写入数据,如果上一轮的数据还未被及时消费,那就是不允许写入的。
这里我们就说一下单线程的吧,当我们点击进来我们会看到下图所示
进入之后,代码如下所示:
public long next(int n) {
// 获取后面坑位的使用权,坑位数量必须大于等于1
if (n < 1) {
throw new IllegalArgumentException("n must be > 0");
} else {
long nextValue = this.pad.nextValue;
long nextSequence = nextValue + (long)n;
long wrapPoint = nextSequence - (long)this.bufferSize;
long cachedGatingSequence = this.pad.cachedValue;
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue) {
long minSequence;
// 如果不能写入,则会走到这个循环,给 parkNanos 住
while(wrapPoint > (minSequence = Util.getMinimumSequence(this.gatingSequences, nextValue))) {
LockSupport.parkNanos(1L);
}
this.pad.cachedValue = minSequence;
}
this.pad.nextValue = nextSequence;
return nextSequence;
}
}
3.2、Disruptor.start()
这里面就会去拿着消费者线程池去创建线程并循环获取环形队列里面生产者生产出来的东西。
public RingBuffer<T> start() {
Sequence[] gatingSequences = this.consumerRepository.getLastSequenceInChain(true);
this.ringBuffer.addGatingSequences(gatingSequences);
this.checkOnlyStartedOnce();
Iterator i$ = this.consumerRepository.iterator();
while(i$.hasNext()) {
ConsumerInfo consumerInfo = (ConsumerInfo)i$.next();
// 拿到消费者线程池并执行 start 方法
consumerInfo.start(this.executor);
}
return this.ringBuffer;
}
最后会走到 BatchEventProcessor 类的run方法,然后执行具体消费者的 onEvent 方法,下面代码其它的都不重要,直接看执行 onEvent 方法那块即可。
public void run() {
if (!this.running.compareAndSet(false, true)) {
throw new IllegalStateException("Thread is already running");
} else {
this.sequenceBarrier.clearAlert();
this.notifyStart();
T event = null;
long nextSequence = this.sequence.get() + 1L;
try {
while(true) {
try {
long availableSequence;
for(availableSequence = this.sequenceBarrier.waitFor(nextSequence); nextSequence <= availableSequence; ++nextSequence) {
event = this.dataProvider.get(nextSequence);
// 执行具体消费者的 onEvent 方法
this.eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
}
this.sequence.set(availableSequence);
} catch (TimeoutException var11) {
this.notifyTimeout(this.sequence.get());
} catch (AlertException var12) {
if (!this.running.get()) {
return;
}
} catch (Throwable var13) {
this.exceptionHandler.handleEventException(var13, nextSequence, event);
this.sequence.set(nextSequence);
++nextSequence;
}
}
} finally {
this.notifyShutdown();
this.running.set(false);
}
}
}
3.3、ByteBuffer.publish()
我们依旧进入单线程的这个实现
public void publish(long sequence) {
// 先把游标向前挪一挪
this.cursor.set(sequence);
// 然后根据具体的等待策略看是否需要唤醒等操作
this.waitStrategy.signalAllWhenBlocking();
}
如果使用的是 BlockingWaitStrategy ,会进行一个线程唤醒的操作:
public void signalAllWhenBlocking() {
this.lock.lock();
try {
this.processorNotifyCondition.signalAll();
} finally {
this.lock.unlock();
}
}