1. Disruptor是什么?
1.1 简介
Disruptor它是一个开源的并发框架,并获得2011年Oracle Duke’s 程序框架创新奖,它的设计初衷是解决高并发场景下队列锁的问题。它最早是由一家英国的外汇公司LMAX(一种新型零售金融交易平台)开发与开源的,能够在无锁的情况下实现对队列的高并发操作,这个系统是建立在JVM平台上,核心是一个业务逻辑处理器,官方号称它能够在单线程里每秒处理6百万订单,业务逻辑处理器完全是运行在内存中,使用事件源驱动方式。业务逻辑处理器的核心是Disruptor。
需要特别说明的是,这里所说的队列是系统内部的内存队列,而非Kafka、RocketMQ这样的分布式队列。
推荐学习网站:
https://ifeve.com/disruptor-getting-started/
https://www.jianshu.com/p/78160f213862
1.2队列
1.2 .1 特性
先进先出(FIFO),先进入队列的元素先出队列(可以理解为我们生活中的食堂排队情况,先排队,先吃饭),生产者(Producer)往队列里面发布事件,消费者(Consumer)获得消息通知并消费,如果队列里面没有事件的时候,消费者堵塞,直到生产者发布了新的事件,消费者再继续消费。
1.2.2 Java中的阻塞队列
ArrayBlockingQueue:基于数组结构组成的有界阻塞队列,使用时必须指定容量大小,内部使用加锁机制保证多线程运行的安全性。
LinkedBlockingQueue:基于链表实现的可选界限的双端阻塞队列,可在构造函数中指定容量,不指定意味着容量为Integer.MAX_VALUE,内部使用加锁机制保证多线程运行的安全性。
ConcurrentLinkedQueue:基于链表实现的线程安全无界非阻塞队列,内部使用CAS操作实现无锁机制保证多线程运行的安全性。
LinkedTransferQueue:基于链表实现的无界阻塞队列,内部使用CAS操作实现无锁机制保证多线程运行的安全性。
总结:通过不加锁的方式实现的队列都是无界的,无界意味着队列长度不可控,若生产者速度过快,则会引起内存溢出。所以在稳定性要求高的系统中,只能选择有界队列,而为了减少Java的垃圾回收对系统性能的影响,会尽量选择数组格式的数据结构,这样筛选下来,只有ArrayBlockingQueue符合了。
1.2.3 Disruptor队列
有界,无锁,多生产者或消费者时才用到CAS操作机制。官方也对disruptor和ArrayBlockingQueue的性能在不同的应用场景下做了对比,目测性能有5~10倍左右的提升。
内部核心RingBuffer数据结构图如下:
2. 为何如此快?
2.1 环形数组结构
1. 数组查询效率高,时间复杂度O(1),而链表查询的时间复杂度为O(n)
2. 用数组实现, 解决了链表节点分散, 不利于cache预读问题
3. 可以预分配用于存储事件内容的内存空间,并且解决了节点每次需要分配和释放, 需要大量的GC问题,此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。
4.环形数组的元素,采用覆盖的方式,避免了JVM的GC。
2.2 无锁
Disruptor根本就不用锁。只在需要确保操作是线程安全的(特别是,在多生产者的环境下,更新下一个可用的序列号)地方,我们使用CAS(Compare And Swap/Set)操作,使用CAS,严格意义上说仍然是使用锁,因为CAS本质上类似是一种乐观锁, 只不过是CPU级别指令, 不涉及到操作系统, 所以效率很高(AtomicLong实现Sequence)。
而单线程的使用普通long,没有锁也没有CAS。这意味着单线程版本会非常快,因为它只有一个生产者,不会产生序号上的冲突。
2.3 数组元素定位
求余操作本身也是一种高耗费的操作, 所以Ringbuffer的size设成2的n次方, 可以利用位操作来高效实现求余。要找到数组中当前序号指向的元素,可以通过mod操作,正常通过sequence % arr.length,优化后可以通过sequence & (arr.length-1)来实现数组下标的获取。比如一共有8槽,seq当前为10,10%8取模后为2,代表应该在数组下标为4的位置,而位操作10&(8-1)=2,HashMap就是用这个方式来定位数组元素的,这种方式比取模的速度更快。
2.4 解决伪共享
2.4.1 缓存行
Cpu cache简单示意图:
CPU是机器的心脏,最终由它来执行所有运算和程序。主内存(RAM)是数据(包括代码行)存放的地方。CPU和主内存之间有好几层缓存,因为直接访问主内存也是非常慢的。其中L1,L2,L3等级缓存都是由缓存行组成的, 通常是64字节, 一个Java的long类型是8字节,因此在一个缓存行中可以存8个long类型的变量. 缓存行是缓存更新的基本单位, 就算你只读一个变量, 系统也会预读其余7个, 并cache这一行, 并且这行中的任一变量发生改变, 都需要重新加载整行, 而非仅仅重新加载一个变量。
2.4.2 伪共享及解决方案-神奇的填充
缓存行的这种免费加载同时也引入了一个弊端,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享,整个缓存行需要从主内存重新读取,从而影响了并发效率,这叫作“伪共享”(译注:可以理解为错误的共享)
你会看到Disruptor消除这个问题,至少对于缓存行大小是64字节或更少的处理器架构来说是这样的(译注:有可能处理器的缓存行是128字节,那么使用64字节填充还是会存在伪共享问题),通过增加补全来确保ring buffer的序列号Sequence不会和其他东西同时存在于一个缓存行中。
public longp1, p2, p3, p4, p5, p6, p7; // cache line padding
private volatile long cursor = INITIAL_CURSOR_VALUE;
public long p9, p10, p11, p12,p13, p14, p15; // cache line padding
3. Disruptor开发示例
从一个简单的事件(消息)生产和消费为例,核心类如下:
UUIDEvent.java 定义事件或消息的数据
public class UUIDEvent {
private String uuid;
public String getUuid() {
return uuid;
}
public void setUuid(String uuid){
this.uuid = uuid;
}
}
UUIDEventFactory.java 定义事件工厂类实例化事件数据对象
public class UUIDEventFactory implements EventFactory
{ /** * 通过事件工厂类实例化事件数据对象 * * @return */
@Override
public UUIDEvent newInstance() {
return new UUIDEvent();
}
}
Consumer.java 定义事件消费者
public class Consumer implements EventHandler
{ private String consumerId;
private static final AtomicInteger ai = new AtomicInteger(0);
public Consumer() {
this.consumerId = "消费者" + ai.getAndIncrement();
}
@Override
public void onEvent(UUIDEvent longEvent, long l, boolean b) throws Exception {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("msg:" + longEvent.getUuid());
}
}
Producer.java 定义事件生产者
public class Producer {
private final RingBuffer
ringBuffer; public Producer(RingBuffer
ringBuffer) { this.ringBuffer = ringBuffer;
}
public void onData(String uuid) {
//可以把ringBuffer看做一个事件队列,那么next就是得到下面一个事件槽 long sequence = ringBuffer.next();
try {
//用上面的序列号取出一个事件用于填充或覆盖
UUIDEvent event = ringBuffer.get(sequence);
event.setUuid(uuid);
} finally {
//发布事件,发布后会提交给消费者进行消费
ringBuffer.publish(sequence);
}
}
}
UUIDEventMain.java 定义运行启动类
public class UUIDEventMain {
public static void main(String[] args) throws InterruptedException {
ThreadFactory threadFactory = Executors.defaultThreadFactory();
UUIDEventFactory factory = new UUIDEventFactory();
int bufferSize = 1024 * 1024;
Disruptor
disruptor = new Disruptor<>(factory, bufferSize, threadFactory, ProducerType.SINGLE, new BlockingWaitStrategy()); disruptor.handleEventsWith(new Consumer()); disruptor disruptor.start();
System.out.println("disruptor启动");
Producer producer = new Producer(disruptor.getRingBuffer());
for (int i = 0; i < 32; i++) { //生产者发布事件 producer.onData(UUID.randomUUID().toString());
}
disruptor disruptor.shutdown();
System.out.println("disruptor关闭");
}
}
4. Disruptor原理分析
4.1 Ringbuffer
RingBuffer是一个环形队列,内部是一个数组和序列号组成,RingBuffer是存储消息的地方,通过一个名为cursor的Sequence对象指示队列的头,协调多个生产者向RingBuffer中添加消息,并用于在消费者端判断RingBuffer是否为空。巧妙的是,表示队列尾的Sequence并没有在RingBuffer中,而是由消费者维护。这样的好处是多个消费者处理消息的方式更加灵活,可以在一个RingBuffer上实现消息的单播,多播,流水线以及它们的组合。其缺点是在生产者端判断RingBuffer是否已满是需要跟踪更多的信息,为此,在RingBuffer中维护了一个名为gatingSequences的Sequence数组来跟踪相关Seqence。
4.2 Producer/Consumer
Producer即生产者,调用Disruptor 发布事件。有两种实现策略,对应的实现类为SingleProducerSequencer、MultiProducerSequencer【都实现了Sequencer类,之所以叫Sequencer是因为他们都是通过Sequence来实现数据读写】 ,它们定义在生产者和消费者之间快速、正确地传递数据的并发算法。具体使用哪个根据自己的场景来定,多线程的策略使用了AtomicLong(Java提供的CAS操作),而单线程的使用long,没有锁也没有CAS。这意味着单线程版本会非常快,因为它只有一个生产者,不会产生序号上的冲突。
Consumer和EventProcessor是一个概念,新的版本中由EventProcessor概念替代了Consumer。
总结:Producer生产event数据存入RingBuffer中,然后发布给消费者,EventHandler或WorkHandler作为消费者消费event并进行逻辑处理。消费消息的进度通过Sequence来控制。
4.3 Sequence
Sequence是Disruptor最核心的组件,上面已经提到过了。生产者对RingBuffer的互斥访问,生产者与消费者之间的协调以及消费者之间的协调,都是通过Sequence实现。几乎每一个重要的组件都包含Sequence。那么Sequence是什么呢?首先Sequence是一个递增的序号,说白了就是计数器;其次,由于需要在线程间共享,所以Sequence是引用传递,并且是线程安全的;再次,Sequence支持CAS操作;最后,为了提高效率,Sequence通过padding来避免伪共享。
总结:Sequence是一个做了缓存行填充优化的原子序列。
4.4 SequenceBarrier
用于保持对RingBuffer的 main published Sequence 和Consumer依赖的其它Consumer的 Sequence 的引用。 Sequence Barrier 还定义了决定 Consumer 是否还有可处理的事件的逻辑。SequenceBarrier用来在消费者之间以及消费者和RingBuffer之间建立依赖关系。在Disruptor中,依赖关系实际上指的是Sequence的大小关系,消费者A依赖于消费者B指的是消费者A的Sequence一定要小于等于消费者B的Sequence,这种大小关系决定了处理某个消息的先后顺序。因为所有消费者都依赖于RingBuffer,所以消费者的Sequence一定小于等于RingBuffer中名为cursor的Sequence,即消息一定是先被生产者放到Ringbuffer中,然后才能被消费者处理。
SequenceBarrier在初始化的时候会收集需要依赖的组件的Sequence,RingBuffer的cursor会被自动的加入其中。需要依赖其他消费者和/或RingBuffer的消费者在消费下一个消息时,会先等待在SequenceBarrier上,直到所有被依赖的消费者和RingBuffer的Sequence大于等于这个消费者的Sequence。当被依赖的消费者或RingBuffer的Sequence有变化时,会通知SequenceBarrier唤醒等待在它上面的消费者。
4.5 WaitStrategy
当消费者等待在SequenceBarrier上时,有许多可选的等待策略,不同的等待策略在延迟和CPU资源的占用上有所不同,可以视应用场景选择:
BusySpinWaitStrategy:自旋等待,类似Linux Kernel使用的自旋锁。低延迟但同时对CPU资源的占用也多。
BlockingWaitStrategy:使用锁和条件变量。CPU资源的占用少,延迟大。
SleepingWaitStrategy:在多次循环尝试不成功后,选择让出CPU,等待下次调度,多次调度后仍不成功,尝试前睡眠一个纳秒级别的时间再尝试。这种策略平衡了延迟和CPU资源占用,但延迟不均匀。
YieldingWaitStrategy:在多次循环尝试不成功后,选择让出CPU,等待下次调。平衡了延迟和CPU资源占用,但延迟也比较均匀。
PhasedBackoffWaitStrategy:上面多种策略的综合,CPU资源的占用少,延迟大。