Disruptor 的大名从很久以前就听说了,但是一直没有时间;看完以后才发现其内部的思想异常清晰,很容易就能前移到其他的项目,所以仔细了解一下还是很有必要的这。篇博客将主要从源码角度分析,Disruptor 为什么那么快,在此之前可以先查看 Disruptor 详解 一 ,能够对 Disruptor 的使用有一个大致的了解;此外 Disruptor 通常会和 ArrayBlockingQueue 做对比,可以参考 JDK源码分析(11)之 BlockingQueue 相关

一、Disruptor 简介

首先可以从下面两张图看到,Disruptor 的内部结构,只这里我偷了一下懒,图中的内容是老版本的,可能和新版本有点不一样但是主要结构还是一样的;


具体使用示例代码我这里就不贴,大家可以看我上一篇博客;

初始化;首先在启动的时候,需要预先初始化 RingBuffer,所以需要传入 EventFactory;这里和 JUC 里面 Queue 很不一样的地方地方是,RingBuffer 中的 Event 不会被取出,每次 publish 的时候都是覆盖之前的内容,所以 RingBuffer 这里是不会产生 GC 的;而生产者和消费者都持有一个 Sequence,指示当前的处理位置,当需要获取 Event 的时候,可以直接使用 sequence & ringBuffer.size - 1 除留余数法快速找到对应的数组位置;

private void fill(EventFactory<E> eventFactory) {
for (int i = 0; i < bufferSize; i++) {
entries[BUFFER_PAD + i] = eventFactory.newInstance();
}
}

生产者;同时可以指定 Disruptor 是单生产者还是多生产者:

  • ProducerType.SINGLE - SingleProducerSequencer :因为只有一个生产者,所以者更新 sequence 的时候是不需要加锁的;
  • ProducerType.MULTI - MultiProducerSequencer :多个生产者的时候,使用乐观锁机制更新 sequence,即 UNSAFE.compareAndSwapLong

当没有空余位置的时候他们都是使用 LockSupport.parkNanos(1L); 来阻塞线程的,如果有需要你也可以改成其他的等待模式;

// RingBuffer
// 首先通过 Sequencer 拿到下一个可用的序列
public long next() { return sequencer.next(); } // 然后用除留余数发拿到对应的数组元素
public E get(long sequence) { return elementAt(sequence); } // 这里是使用 UNSAFE 直接获取内存对象
protected final E elementAt(long sequence) {
return (E) UNSAFE.getObject(entries, REF_ARRAY_BASE + ((sequence & indexMask) << REF_ELEMENT_SHIFT));
} // 最后将拿到的数组元素修改为新的 Event,再发布
public void publish(long sequence) { sequencer.publish(sequence); } // 这里所有关于生产者并发的问题都封装到了 Sequencer 里面,后面最详细讲到

消费者;正因为上面说的 RingBuffer 中的对象不对像普通的 Queue 一样,真正取出,所以在 Disruptor 中可以很容易做到,同一个消息同时被多个消费者获取的逻辑;这里的关键就在于 每个消费者所持有的 Sequence;

  • 当消息可以被重复消费的时候,每个消费者不需要管其他的消费者,每次获取新任务的时候,只需要和生产者的 Sequence 比较就可以了,获取成功后更新自己,这样每个消费者就可以互不影响了;
  • 当消息不能被重复消费的时候,所有的消费者共享一个 Sequence,当发生竞争的时候使用指定的 WaitStrategy 解决冲突;

等待策略;Disruptor 提供了很多从等待策略,这里需要根据实际的业务需求选择使用;同时和 JDK 中的队列相比,无论是阻塞队列还是并发队列,其控制并发的方式都是固定的,而在 Disruptor 中则可以很容易的定制这些策略,从这一点来看也可以说是实现了策略模式;

  • BlockingWaitStrategy: 和 ArrayBlockingQueue 一样使用加锁的方式
  • BusySpinWaitStrategy: Busy Spin strategy 自旋等待
  • LiteBlockingWaitStrategy: BlockingWaitStrategy 的变种,也是使用加锁方式
  • PhasedBackoffWaitStrategy:两段式策略
  • SleepingWaitStrategy: 这是一个在性能和 CPU 占用率做了平衡的一种策略,初始自旋,然后 Yield,最后 Sleep
  • TimeoutBlockingWaitStrategy:同名字
  • YieldingWaitStrategy: 使用自旋、Yield 方式

以上这些就是 Disruptor 的大致框架性内容了,另外有两点是 Disruptor 很快的重要原因;

  • 缓存的引用
  • 并发的处理

二、Disruptor 对缓存的应用

首先计算机中各级存储器的速度差异巨大,数量级描述大致如下:

存储器 容量 速度
寄存器 * / B 1 ns
一级 Cache * / KB 5 ~ 10 ns
二级 Cache * / KB - M 40 ~ 60 ns
内存 */ M - G 100 ~ 150 ns
硬盘 * / G - T 3 ~ 15 ms

根据上图的数据,直观的反应如果想加快软件的运行速度,当然是尽量利用上层的缓存体系;在 JVM 中缓存不是以单字节存在的,而是以缓存行的形式,通常是 2 的整数幂个连续字节,一般为 32-256 个字节。最常见的缓存行大小是 64 个字节;

在我们的队列,数则或者 Disruptor 中,理想状态下就是生产者和消费的速度保持相对一致,这样能避免阻塞的发生,其生产者和消费者就分别位于数组的头部和尾部;

但是这样的理想状态很难到达,要么是生产者快一些,要么是消费者快一些,其结果如下图;

所以头和尾通常都位于同一个缓存行中,这样者更新头的时候,将对应的缓存标记为失效,同时尾也被标记为了失效,者就是伪缓存;

下面是一个缓存的测试例子;

public final class FalseSharing implements Runnable {
private static final int NUM_THREADS = 4; // change
private static final long ITERATIONS = 500L * 1000L * 1000L;
private final int arrayIndex;
private static VolatileLong[] longs = new VolatileLong[NUM_THREADS]; static {
for (int i = 0; i < longs.length; i++) {
longs[i] = new VolatileLong();
}
} public FalseSharing(final int arrayIndex) {
this.arrayIndex = arrayIndex;
} public static void main(final String[] args) throws Exception {
final long start = System.nanoTime();
runTest();
System.out.println("duration = " + (System.nanoTime() - start));
} private static void runTest() throws InterruptedException {
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new FalseSharing(i));
}
for (Thread t : threads) {
t.start();
} for (Thread t : threads) {
t.join();
}
} @Override
public void run() {
long i = ITERATIONS + 1;
while (0 != --i) {
longs[arrayIndex].value = i;
}
} public static final class VolatileLong {
// public long p1, p2, p3, p4, p5, p6; // cache line padding
public volatile long value = 0L;
// public long p8, p9, p10, p11, p12, p13, p14, p15; // cache line padding
}
}

这里不同的机器测试的结果不同,大家可以修改线程数,padding 数,和 padding 的先后顺序;会得到不同的结果;

我测试的结果:

无 padding :17988876300

有 padding :4667271000

可以看到是查了一个数量级

这样的缓存填充,在 Disruptor 中随处可见:

abstract class RingBufferPad {
protected long p1, p2, p3, p4, p5, p6, p7;
} public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E> {
public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE;
protected long p1, p2, p3, p4, p5, p6, p7;
...
}

三、Disruptor 的并发处理

并发的处理,同样的 Disruptor 中随处可见,虽然在平时写代码的时候也会注意,但是当状态变量多了以后,代码就会变得很复杂,不容易读懂;而在 Disruptor 中由 Sequence 串联起来的各个部分,以及策略模式的应用,使得每部分的处理一样的清晰;这里的内容太多了就不一一分析了,比如 MultiProducerSequencer 和 SingleProducerSequencer;

// SingleProducerSequencer
public long next(int n) {
if (n < 1) throw new IllegalArgumentException("n must be > 0");
long nextValue = this.nextValue;
long nextSequence = nextValue + n;
long wrapPoint = nextSequence - bufferSize;
long cachedGatingSequence = this.cachedValue; if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue) {
cursor.setVolatile(nextValue); // StoreLoad fence long minSequence;
while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue))) {
LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
} this.cachedValue = minSequence;
} this.nextValue = nextSequence;
return nextSequence;
} // MultiProducerSequencer
public long next(int n) {
if (n < 1) throw new IllegalArgumentException("n must be > 0");
long current;
long next; do {
current = cursor.get();
next = current + n; long wrapPoint = next - bufferSize;
long cachedGatingSequence = gatingSequenceCache.get(); if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current) {
long gatingSequence = Util.getMinimumSequence(gatingSequences, current); if (wrapPoint > gatingSequence) {
LockSupport.parkNanos(1); // TODO, should we spin based on the wait strategy?
continue;
}
gatingSequenceCache.set(gatingSequence);
}
else if (cursor.compareAndSet(current, next)) {
break;
}
}
while (true);
return next;
}

总结

对 Disruptor 源码查看的最大感觉是,习以为常的结构设计模式,都可以有更精妙的写法,如果 Sequence 承担的各部分逻辑串联的角色,整体的消费者生产者模式,消费者部分可以看成是观察者模式,也可以看出是事件监听模式,以及并发控制的策略模式;两外就是包括伪缓存在内的各细节优化;

Disruptor 详解 二的更多相关文章

  1. .NET DLL 保护措施详解(二)关于性能的测试

    先说结果: 加了缓存的结果与C#原生代码差异不大了 我对三种方式进行了测试: 第一种,每次调用均动态编译 第二种,缓存编译好的对象 第三种,直接调用原生C#代码 .net dll保护系列 ------ ...

  2. PopUpWindow使用详解(二)——进阶及答疑

      相关文章:1.<PopUpWindow使用详解(一)——基本使用>2.<PopUpWindow使用详解(二)——进阶及答疑> 上篇为大家基本讲述了有关PopupWindow ...

  3. Android 布局学习之——Layout(布局)详解二(常见布局和布局参数)

    [Android布局学习系列]   1.Android 布局学习之——Layout(布局)详解一   2.Android 布局学习之——Layout(布局)详解二(常见布局和布局参数)   3.And ...

  4. logback -- 配置详解 -- 二 -- <appender>

    附: logback.xml实例 logback -- 配置详解 -- 一 -- <configuration>及子节点 logback -- 配置详解 -- 二 -- <appen ...

  5. 爬虫入门之urllib库详解(二)

    爬虫入门之urllib库详解(二) 1 urllib模块 urllib模块是一个运用于URL的包 urllib.request用于访问和读取URLS urllib.error包括了所有urllib.r ...

  6. [转]文件IO详解(二)---文件描述符(fd)和inode号的关系

    原文:https://www.cnblogs.com/frank-yxs/p/5925563.html 文件IO详解(二)---文件描述符(fd)和inode号的关系 ---------------- ...

  7. Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

    View 的绘制系列文章: Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImpl 在上一篇  ...

  8. HTTPS详解二:SSL / TLS 工作原理和详细握手过程

    HTTPS 详解一:附带最精美详尽的 HTTPS 原理图 HTTPS详解二:SSL / TLS 工作原理和详细握手过程 在上篇文章HTTPS详解一中,我已经为大家介绍了 HTTPS 的详细原理和通信流 ...

  9. Linux dts 设备树详解(二) 动手编写设备树dts

    Linux dts 设备树详解(一) 基础知识 Linux dts 设备树详解(二) 动手编写设备树dts 文章目录 前言 硬件结构 设备树dts文件 前言 在简单了解概念之后,我们可以开始尝试写一个 ...

随机推荐

  1. WPF 多路绑定

    using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threa ...

  2. C++成员函数指针错误用法警示(成员函数指针与高性能的C++委托,三篇),附好多评论

    今天做一个成绩管理系统的并发引擎,用Qt做的,仿照QtConcurrent搞了个模板基类.这里为了隐藏细节,隔离变化,把并发的东西全部包含在模板基类中.子类只需注册需要并发执行的入口函数即可在单独线程 ...

  3. Grokking PyTorch

    原文地址:https://github.com/Kaixhin/grokking-pytorch PyTorch is a flexible deep learning framework that ...

  4. Win8 Metro(C#)数字图像处理--2.52图像K均值聚类

    原文:Win8 Metro(C#)数字图像处理--2.52图像K均值聚类  [函数名称]   图像KMeans聚类      KMeansCluster(WriteableBitmap src,i ...

  5. SQLSERVER 数据库备份脚本-支持多库备份

    原文:SQLSERVER 数据库备份脚本-支持多库备份 <pre name="code" class="sql">--变量定义 DECLARE @b ...

  6. DataTable 更改在有数据列的类型方法

    原文:DataTable 更改在有数据列的类型方法 /// <summary> /// 修改数据表DataTable某一列的类型和记录值(正确步骤:1.克隆表结构,2.修改列类型,3.修改 ...

  7. 配置QSslConfiguration让客户端程序跳过本地SSL验证

    大家下午好哦.今天我们在重新制作我们萌梦聊天室的时候,出现了这样的问题.那就是我们的客户端能够对qtdream.com服务器进行登录,但是不能对localhost服务器(也就是本机啦)进行登录.这究竟 ...

  8. 亿方云(用电话或者qq沟通是远远不够的,容易忘还不能反复催,最好的方式就是指定一个平台,团队内的人定期查看最新记录)

    作者:城年链接:http://www.zhihu.com/question/20579359/answer/106319200来源:知乎著作权归作者所有,转载请联系作者获得授权. 更新,文字发完后,好 ...

  9. Realm_King 之 XPDL(XML Process Definition Language)

    XPDL(XML Process Definition Language)是由Workflow Management Coalition(简写为:WfMC)所提出的一个标准化规格,使用XML文件让不同 ...

  10. SYN2306型 北斗串口时间服务器

    SYN2306型  北斗串口时间服务器  北斗授时设备北斗时钟同步系统使用说明视频链接: http://www.syn029.com/h-pd-108-0_310_36_-1.html 请将此链接复制 ...