Disruptor高性能之道—无锁实现(CAS)

旧时模样 提交于 2020-01-14 04:43:49

目录

一、前言

二、RingBuffer简介

三、依赖链

四、无锁竞争实现

1、生产者和消费者竞争实现

2、生产者和生产者竞争实现

3、消费者和消费者竞争实现

4、消费组内的消费者竞争实现

5、实现总结

五、惯例


 

一、前言

说到Disruptor都会提到其牛逼的性能,一说其性能大家都应该会想到它的无锁实现。大家都会说因为它是无锁实现的(重要因素,并不是唯一因素),所以它的性能很好。

那么它是怎样通过无锁的方式解决了多线程(消费者和生产者)之间的竞争呢?本文将给大家做一个基本的分析。

 

二、RingBuffer简介

熟悉或者了解Disruptor同学都知道,Disruptor采用了RingBuffer的数据结构来存储数据,而不是像BlockingQueue那样使用的队列。RingBuffer就是一个基于数据组实现的环形结构(如果还有不了解的同学,可以先行百度了解下)。生产者每生产一个数据就向前移动一格,而消费者每消费一个数据也向前移动一格。

图片来自网络

另外RingBuffer的大小是创建的时候就固定的,且其会预先分配好所有内存。即每个位置都会提前填充好对应的数据对象(一般是一个空对象)。然后每次生产者生成数据的时候仅仅是拿到这个位置的数据对象,然后将数据填充到(如通过setter)该对象中(该步骤由业务代码自己完成)。这样就避免了因频繁的创建和销毁对象而导致的不必要的对象回收。当然没有频繁的创建对象,对性能也有不小的提升,比较创建对象对应JVM来说还是一个比较昂贵的操作。

 

三、依赖链

在具体介绍无锁实现之前,需要先和大家一起回顾下Disruptor中生产者和消费者(组)之间的关系。在Disruptor中,生产者和消费者(组)实际上是构成了一个依赖链。我们一个简单的场景为例,比如有1个生产者,3个消费组且消费组之间彼此依赖。构成如下依赖链(图中只是一个简化的依赖关系,实际的依赖关系比这个复杂,比如实际上每个消费组都会依赖生产者):

图中大致的依赖关系是如下:

  1. 当消费组1追赶上生产者后,消费组1需要等待生产者1生成更多的数据才能够继续消费

  2. 当消费组2追赶消费组1后,消费组2需要等待消费组1消费完成后才能消费

  3. 当消费组3追赶消费组2后,消费组3需要等待消费组2消费完成后才能消费

  4. 当生产者1追赶上消费组3后,生产者1需要等待最后一个消费组3消费了才能够继续生成数据

 

四、无锁竞争实现

结合上图的依赖链我们可以知道,在生产者和消费者(组)之间存在数据竞争的场景一般有如下几种:

  1. 生产者和消费组竞争同一个数据(RingBuffer环中的某个数据对象)

  2. 生产者和生产者(多生产者)竞争同一个数据(up)

  3. 消费组和消费组竞争同一个数据位置(判断是否可以消费)

  4. 消费组内的消费者之间竞争同一个消息数据(RingBuffer中某个槽位的数据)

1、生产者和消费者竞争实现

这种场景我们主要考虑当生产者追赶上了依赖链中最后一个消费者(组)的时候,此时生产者就无法继续生产数据了,必须要等待该消费者进一步消费释放出可用槽位。那么生产者是如何判断其追赶上了消费者的呢?当生产者追赶上了消费者(组)之后它是如何进一步处理的呢?是加锁?还是wait?

第一个问题其实比较简单,因为生产者中存储了最后一个生产者(组)的Sequence(标记当前消费的位置,讲解False Sharing中有提到过),所以生产者只需要不断循环判断消费者的位置是否大于生产者的当前位置即可,如果生产者的Sequence = 最后一个消费者的Sequence,就表示生产者追赶上了消费者。

那么第二个问题追赶上了之后如何进一步处理。其实也比较简单,此时生产线程会不断循环判断是否有可用槽位(实践实现的时候,有睡眠1毫秒的操作,具体看代码截图)。所以这里通过了不断循环的方式了防止了使用锁。

注:如果这里使用wait/notify机制,会间接的引入锁,因为wait必须在同步块中使用。

2、生产者和生产者竞争实现

在多生产者的场景下,存在资源竞争。即多个生产者都要去竞争当前可用的槽位。比较简单的解决办法就是加锁,防止多线程竞争RingBuffer中的可用槽位。那么无锁怎么实现呢?其实也很简单,就是参考AtomicInteger的实现,使用CAS+不断循环的逻辑即可。

如下是Disruptor的多生产者竞争的代码,就是普通的CAS实现思想。具体就不细讲了,如果有不太了解的同学可以看下AtomicInteger的源码。

3、消费者和消费者竞争实现

其实消费者和消费者的竞争实现和“生产者和消费者竞争实现”差不多,就是下游消费者不断去判断和上游消费者之间是否有需要可消费的数据,没有不断循环等待。不同的是,这里循环中并没有park,而是使用了Thread的onSpinWait。这是Java9中的一个新方法。大概作用就是优化不暂停循环的代码块。图中有我添加的注释大家大致可以看下,如果还是不了解的同学可以自行百度深入了解下。

第一个消费者依赖生产者也是这段代码逻辑。即第一个消费者把生产者当成了上游消费者处理。

4、消费组内的消费者竞争实现

在Disruptor中消费组内有两种消费策略:

  1. 每个消息只被一个消费者消费

  2. 每个消息可以被所有消费者消费

很明显,如果是第二种策略,消息可以被所有消费者消费,那么消费者彼此之间就不存在竞争。但如果是第一种策略,则消费者只能够被一个消费者消费,则消费者之间就存在竞争。即大家要争抢谁来消费这个数据。​它的实现也比较简单,也是CAS的思想,这里不再赘述,直接上源码。

5、实现总结

通过上述分析,我们可以看到Disruptor无锁实现思想主要是如下两种:

  1. CAS思想

  2. 无限循环+短暂park(暂停)

其总体思想就是,“浪费”CPU时钟来提升速度。上述两种思想都会额外的“浪费”很多的CPU时钟(不断循环会消耗很多CPU时钟),但是其避免了使用锁,所以能够提升性能。这种通过“浪费”CPU时钟的方式来提升性能的思想在高并发组件或者实现中还是比较常见的。

 

五、惯例

如果你对本文有任何疑问或者高见,欢迎添加公众号共同交流探讨(添加公众号可以获得”Java高级架构“上10G的视频和图文资料哦)。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!