一、Disruptor基本原理
在多线程开发中,我们常常遇到这样一种场景:一些线程接受用户请求,另外一些线程处理这些请求。比如日志处理中的日志输入和告警。这种典型的生产者消费者场景十分常见,而生产者消费者模式的核心就是阻塞队列。由于阻塞队列会涉及大量的锁竞争和线程阻塞,都是非常耗费CPU的操作,因此阻塞队列的性能好坏能够在很大程度上决定上层应用的性能瓶颈。
JAVA中用BlockingQueue这个接口来描述阻塞队列,有数组实现的有界阻塞队列为 ArrayBlockingQueue,用链表实现的无界阻塞队列为LinkedBlockingQueue,除此之外还有优先级阻塞队列 PriorityBlockingQueue,这些阻塞队列除了自身特有逻辑外,都采用基于悲观锁的并发控制。这样的并发机制会有严重的锁冲突,大大影响并发性能。Disruptor满足了我们的要求。
锁是用来做并发最简单的方式,当然其代价也是最高的。内核态的锁的时候需要操作系统进行一次上下文切换,等待锁的线程会被挂起直至锁释放。
使用了Ringbuffer,内存屏障,乐观并发控制等众多优化手段后,Disrupter的阻塞队列与传统的阻塞队列相比有超过10倍的吞吐率。
Disruptor的主要设计思想是无锁的高并发,在设计上采用内存屏障的机制和CAS操作实现此思想。主流的并发程序 都离不开锁对资源的管控,或者尽量避开锁的使用。
其主要的实现原理总结有如下三点:
-
采用消费者-生产者模型进行读写的分离。
-
用循环缓存(实际是一个循环队列)实现了数据的暂存和读写速度的匹配。
-
用内存屏障加序列号的方式实现了无锁的并发机制。
为什么Disruptor的速度这么快?
我们知道Disruptor速度很快,但是为什么呢?
Disruptor没有使用很影响性能锁 。取而代之的是,在需要确保操作是线程安全的(特别是,在多生产者的环境下,更新下一个可用的序列号)地方,我们使用CAS(Compare And Swap/Set)操作。这是一个CPU级别的指令,它的工作方式有点像乐观锁——CPU去更新一个值,但如果想改的值不是原来的值,操作就失败,反之则去更新这个值。
说句题外话,Java中AtomicInteger也使用了CAS操作来保证原子性。在并发控制中,CAS操作是十分重要的。
CAS操作是CPU级别的指令,在Java中CAS操作在Unsafe类中(Unsafe,见名知意,这个类是不安全的,不建议在实际开发的时候使用)。关于CAS操作的原理网上有很多,在此不过多说明了。
另一个重要的因素是Disruptor消除了伪共享。 下面引用网上的一段话,来解释下什么是伪共享。
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是 2 的整数幂个连续字节,一般为 32-256 个字节。最常见的缓存行大小是 64个字节。
当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在 SMP 系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。
第三个原因是Disruptor采用了RingBuffer。Ring Buffer 是一个数组,它比链表要快,而且有一个容易预测的访问模式,数据可以在硬件层面预加载到高速缓存,极大的提高了数据访问的速度。
RingBuffer可以预先分配内存,并且保持数组元素永远有效。这意味着内存垃圾收集(GC)在这种情况下几乎什么也不用做。
二、Disuptor基本概念
-
RingBuffer:Ringbuffer是一个环形缓冲区,是Disruptor的核心。
-
Sequencer:序号管理器,使消费者和生产者之前快速正确地传输数据。
-
Sequence:RingBuffer中每个数据的序号,用于跟踪ringbuffer中任务的变化和消费者的消费情况。
-
SequenceBarrier:序号栅栏,管理和协调生产者的游标序号和各个消费者的序号,确保生产者不会覆盖消费者未来得及处理的消息,确保存在依赖的消费者之间能够按照正确的顺序处理。
-
Event:生产者和消费者之间进行交换的数据称为事件。
-
EventProcessor:事件处理器,监听RingBuffer的事件,并消费可用事件,从RingBuffer读取的事件会交由实际的生产者实现类来消费;它会一直侦听下一个可用的序号,直到该序号对应的事件已经准备好。
-
EventHandler:业务处理器,是实际消费者的接口,完成具体的业务逻辑实现,第三方实现该接口;代表着消费者。
-
Producer:生产者接口,第三方线程充当该角色,producer向RingBuffer写入事件。
本文引用了很多网上的博客内容,由衷地感谢他们乐于分享的精神。
网友评论