Disruptor并发<一> _ 介绍

目录

一、介绍

二、为什么速度更快?

1. JDK中队列加锁

2. Disruptor无锁机制

三、Disruptor特点

1. 无锁

2. 预分配

3. 缓存行填充

4. 批操作

四、核心组件

五、参考资料


一、介绍

        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博客

百度安全验证

Disruptor介绍及原理讲解 - 简书

Disruptor3.0的实现细节 - 倒骑的驴 - 博客园

并发框架Disruptor浅析 - shenck - 博客园

Disruptor 系列(二)使用场景 - 走看看

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值