原文地址:http://mechanitis.blogspot.com/2011/08/dissecting-disruptor-why-its-so-fast.html, 作者是 Trisha Gee, LMAX 公司的一位女工程师。
我最近写文章的速度变慢了,是因为我一直在尝试写一篇博客解释内存屏障(Memory Barrier)以及它在 Disruptor 的应用。问题是,无论我阅读了多少次,无论我向永远耐心的 Martin 和 Mike 提出多少个问题试图弄明白一些重点,我还是没办法直观的把握主题。我猜我没有完全理解它所需要的深厚背景知识。
因此,与其让一个像我这样的傻瓜去解释一些连自己都没有真正了解的东西,我准备尝试和覆盖的,是在一个抽象和大量简化的层面、我确实已经在这个领域内弄懂的东西。Martin 写过一篇详细的 走进内存屏障(Memory Barrier),这太有助于我回避和忽略本文的主题了。
免责申明:在这篇解答里的任何错误全都是我自己的事,并不反映 Disruptor 的实现或者真正理解这个东西的 LMAX 同事水平。
重点是?
在这一系列的博客中,我的主要目标是解释 Disruptor 是如何工作的——并且,稍微小小的扩展到,它为什么这样工作。理论上,从想要使用它的开发者视角描述 Disruptor,我可以在代码与 技术文献 间建立一座桥梁。
本文提到 Memory Barrier(内存屏障),我打算理解它是什么,并且如何使用。
什么是 Memory Barrier(内存屏障)?
这是一条 CPU 指令。是的,再一次,我们去思考 CPU 层面的特性以获得我们需要的性能(请参考 Martin 著名的 Mechanical Sympathy 理论)。基本上它是一条这样的指令:a) 保证特定操作的执行顺序,以及 b) 影响某些数据(也许是某些指令的执行结果)的可见性。
编译器和 CPU 能够重排序指令,保证最终相同的结果,并尝试优化性能。插入一条 Memory Barrier 会告诉 CPU 和编译器:在这条命令之前发生的必须待在这条命令之面,在这条命令之后的必须待在命令之后。所有这一切都像一趟拉斯维加斯之旅完全占据了你的脑子一样。
Memory Barrier(内存屏障)所做的另一件事是强制刷出各种 CPU cache(高速缓存)——比如,一个 Write-Barrier(写入屏障)将刷出所有在 Barrier 之前写入 cache 的数据,因此,任何尝试去读这些数据的线程将会读到它们的最新版本,而不管线程在哪个 CPU 核心或者哪个 CPU 插槽(Socket)上执行。
这与 JAVA 有什么关系?
我知道你在想什么——这不是汇编,是 Java。
这里的魔法咒语是关键字 "volatile"(我觉得这是 Java 认证里从来没有解释清楚过的东西)。如果你的字段是 volatile 的,Java 内存模型(Java Memory Model)会在你写入字段之后插进一个 Write-Barrier 指令,并且在你读这个字段之前插入一个 Read-Barrier 指令。
这意味着,如果写入一个 volatile 字段,你知道这些事会发生:
1. 从写入字段这个时间点开始,任何线程访问该字段都会拿到更新后的数据。
2. 在写入字段之前的操作都确实执行过了,并且它们更新的数据也是可见的,
因为 Memory Barrier 会刷出 cache 中所有先前的写入。
请举例!
很高兴你提出这个要求。这是我又要开始画“甜甜圈”的时间了。
RingBuffer 游标字段(cursor)是这些神奇的“volatile ”变量之一,它也是我们可以不用锁而实现 Disruptor 的一个原因。
生产者(Producer)先拿到下一个(或者下一批)Entry 对象,然后随意访问这些对象,用各种想写入的值更新它们。与 你知道的 一样,在全部更新结束后,生产者会调用 RingBuffer 的 commit 方法,由它去更新序号。这个对“volatile”字段(cursor)的写入产生了一个 Memory Barrier,最终它会刷出所有的 cache(或者至少让它们失效)。
在这个时间点之后,消费者会拿到最新的序号(8),因为 Memory Barrier 也同样保证先前发生的 CPU 指令顺序,因此消费者可以确信生产者对序号 7 之上的 Entry 对象所做的全部修改也都生效了。
... 在消费者那边呢?
消费者上的序号也是“volatile”变量,它被一系列外部对象读取——其他的 下游 消费者可能在追踪这个消费者,而且 ProducerBarrier/RingBuffer(取决于你看的是老代码还是新代码)也会追踪它以保证环不会重叠(wrap)。
因此,如果下游消费者(C2)看到前面的消费者(C1)读到了序号 12,它接着从 RingBuffer 读序号 12 之前的节点时,就可以读到消费者(C1)更新序号前对节点做的全部更改。
基本上,消费者(C2)拿到更新序号后做的一切操作(在上图用蓝色表示)都必须发生在消费者(C1)在更新序号前对 RingBuffer 所做的一切操作(用黑色表示)之后。
对性能的影响
内存屏障(Memory Barrier)作为另一种 CPU 级别的指令,不像 加锁的开销 那么大 —— 操作系统内核并不需要在多个线程间调度和协调。但是任何东西都不是免费的。内存屏障的确有一些开销 —— 编译器/CPU 不能重排序指令,会潜在的导致代码没有尽可能高效的利用 CPU,而且刷新 CPU cache 会对性能有明显的影响。因此,不要以为用“volatile”字段代替锁就能让你永远逍遥法外。
你会注意到 Disruptor 在实现里尝试尽可能少的读写序号。每次读写“volatile”字段都是一次相对开销较大的操作。认识到这一点可以很好的进行批处理 —— 如果你知道不应该太频繁的读写序号,那么先抓取一批节点进行处理,然后再更新序号 —— 这样无论对于生产端和消费端都是有意义的。下面是一个来自 BatchConsumer 的例子:
long nextSequence = sequence + 1; while (running) { try { final long availableSequence = consumerBarrier.waitFor(nextSequence); while (nextSequence <= availableSequence) { entry = consumerBarrier.getEntry(nextSequence); handler.onAvailable(entry); nextSequence++; } handler.onEndOfBatch(); sequence = entry.getSequence(); } ... catch (final Exception ex) { exceptionHandler.handle(ex, entry); sequence = entry.getSequence(); nextSequence = entry.getSequence() + 1; } }
(你会注意到这儿还是“老”代码和命名规则,因为这篇文章紧接着我的上一篇博客,我想这样比直接切换到新命名规则会稍微减少一些混乱)
在上面的代码中,我们在消费者处理节点的循环中递增的是一个局部变量。这意味着我们读写 sequence 这个 volatile 字段(用粗体表示)的次数尽可能的降到了最低。
总结
内存屏障(Memory Barrier)是一个 CPU 指令,它允许你对数据在什么时候被其他进程可见作出确定的假设。在 Java 中,你可以用 volatile 关键字来实现它们。使用 volatile 关键字意味着你不需要被迫的、别无选择的加锁,而且使用还会让你获得性能提升。但是,这需要更加小心的思考你的设计,特别是你对 volatile 字段的使用有多频繁,以及读写它们有多频繁。
PS: 鉴于 Disruptor 当前的“世界新秩序”与我至今为止博客里提到的一切相比,用的是完全不同的命名规则,我想下一篇文章是该把“旧世界”映射到“新世界”了。
译注
这是 Trisha Gee 博客里有关 Disruptor 原理介绍的最后一篇,其他的几篇介绍大家可以阅读我博客中的译文:
相关推荐
内存屏障(Memory Barrier)是一种硬件指令,用于控制处理器的内存访问顺序,确保特定类型的内存访问按顺序完成。Disruptor利用内存屏障来保证在无锁设计中,数据的可见性和一致性,从而避免了竞态条件的发生。 ###...
- **Ring Buffer**:Disruptor的核心数据结构是一个环形缓冲区,它避免了锁和内存屏障带来的性能开销,通过固定大小的缓存块进行数据交换。 - **Sequencer**:负责为生产者和消费者分配唯一的序列号,确保数据的...
6. **屏障(Barrier)**:在Disruptor中,屏障用于确保事件的正确顺序。一个屏障可以确保在某个事件被所有消费者处理完之前,后续的事件不会被处理。这样,即使有多个消费者,也能保持数据的正确流程。 7. **无锁...
Disruptor的高效性能得益于其无锁设计和避免了传统的内存屏障。此外,其采用的序列号机制确保了数据的一致性,而无需使用传统的锁或volatile变量。在实际应用中,Disruptor通常与其他并发工具如ExecutorService结合...
4. **Barrier**:屏障(Barrier)是Disruptor中的一个重要概念,它确保一组事件被处理完后,后续事件才能被处理。例如,多个消费者可以形成一个链式处理,每个消费者都有自己的屏障。 **Disruptor的设计优势** 1. ...
4. **屏障(Barrier)与门(Gate)**:Disruptor中的屏障确保了事件处理的正确顺序。例如,BatchEventProcessor使用一个屏障来等待所有依赖的处理器完成对事件的处理,确保数据的一致性。 5. ** ClaimStrategy 和 ...
netty结合disruptor队列实现即时通信1、简介使用disruptor改造netty通讯,使提高吞吐率,主要是提供disruptor如何与netty整合的思路2、软件架构spring-boot2.7.3 + netty4.1.36.Final + disruptor + jdk1.83、源码...
4. **屏障(Barrier)**:Disruptor使用屏障来同步生产者和消费者的进度,确保在某个屏障点之前的所有事件都被处理完毕,之后的事件才能被处理。 二、Disruptor的多线程优化 1. **无锁算法**:Disruptor通过使用...
《Disruptor原始码解析-源码解析》 Disruptor是英国LMAX公司开发的一款高性能、低延迟的并发框架,它在处理高并发场景时展现出卓越的性能,被誉为金融交易领域的“神器”。本篇文章将深入探讨Disruptor的设计原理,...
Disruptor是一款高性能的并发框架,它通过使用Ring Buffer和基于事件的处理方式来消除锁竞争,提升系统性能。在使用Disruptor过程中,开发者可能会遇到`FatalExceptionHandler`的错误,这通常是由于处理流程中的异常...
《Spring Boot Starter Disruptor深度解析》 在现代软件开发中,高性能和低延迟往往是系统设计的关键要素。Spring Boot作为Java领域最受欢迎的微服务框架,提供了丰富的启动器(starters)来简化开发工作。"spring-...
- **无锁并发**:Disruptor采用内存屏障和原子性的CAS操作代替了传统的锁机制,从而减少了线程间的竞争开销。 - **基于数组的缓存**:Disruptor使用固定大小的环形数组作为内部缓存,通过位运算优化寻址操作,提高...
《Disruptor技术详解——基于DisruptorDemo.zip实例解析》 Disruptor,由LMAX公司开发并开源,是一款高性能、低延迟的并发工具,主要用于优化多线程间的通信。它采用一种环形缓冲区(Ring Buffer)的设计,极大地...
赠送jar包:disruptor-3.3.0.jar; 赠送原API文档:disruptor-3.3.0-javadoc.jar; 赠送源代码:disruptor-3.3.0-sources.jar; 赠送Maven依赖信息文件:disruptor-3.3.0.pom; 包含翻译后的API文档:disruptor-...
赠送jar包:disruptor-3.3.7.jar 赠送原API文档:disruptor-3.3.7-javadoc.jar 赠送源代码:disruptor-3.3.7-sources.jar 包含翻译后的API文档:disruptor-3.3.7-javadoc-API文档-中文(简体)-英语-对照版.zip ...
赠送jar包:disruptor-3.3.0.jar; 赠送原API文档:disruptor-3.3.0-javadoc.jar; 赠送源代码:disruptor-3.3.0-sources.jar; 赠送Maven依赖信息文件:disruptor-3.3.0.pom; 包含翻译后的API文档:disruptor-...
此外,Disruptor还支持多个消费者并行消费,通过顺序屏障(Barrier)确保事件的正确顺序。 Disruptor的应用实例通常包括以下几个步骤: 1. 初始化:创建Disruptor对象,设置环形缓冲区的大小,以及需要的事件...
2. **消除缓存失效**:由于所有生产者和消费者共享同一个内存区域,所以缓存局部性得到优化,减少了缓存失效的开销。 3. **预定义的序列号**:Disruptor 提供全局唯一的序列号,确保消息的正确顺序。 在 Netty 中,...