当前位置: 首页 > news >正文

Disruptor高性能基石:Sequence并发优化解析

Sequence

Sequence 类本质上是一个支持并发操作的计数器,主要用于跟踪 Ring Buffer 的进度以及各个事件处理器(EventProcessor)的处理进度。为了追求极致的性能,它在设计上包含了两个核心思想:防止伪共享(False Sharing) 和 精细的内存屏障控制

核心设计一:通过缓存行填充(Cache Line Padding)防止伪共享

这是 Sequence 类最广为人知的一个特性。当看到 LhsPaddingValueRhsPadding 这几个内部类时,就应该立刻联想到这个优化。

在现代多核 CPU 中,为了提高数据访问速度,每个核心都有自己的高速缓存(Cache)。缓存的最小管理单位是缓存行(Cache Line),通常是 64 字节。当 CPU 从主内存加载一个变量时,它会把该变量及其周围的内存数据一同加载到一个缓存行中。

如果两个线程在不同的核心上运行,并且它们需要频繁修改的两个不同变量恰好位于同一个缓存行中,就会发生“伪共享”问题。当一个线程修改了其中一个变量,会导致整个缓存行失效,另一个核心就必须重新从主内存加载数据,即使它关心的那个变量并未被修改。这种不必要的缓存失效和数据同步会严重影响性能。

Sequence 通过继承一系列类,在真正需要存储的 long 类型 value 值前后,填充了大量无用的字节变量,目的就是确保这个 value 能够独占一个或多个缓存行,从而避免和其他变量发生伪共享。

我们来看一下它的继承结构:

// ... (其他代码)
class LhsPadding
{protected bytep10, p11, p12, p13, p14, p15, p16, p17,// ... (大量字节填充)p70, p71, p72, p73, p74, p75, p76, p77;
}class Value extends LhsPadding
{protected long value;
}class RhsPadding extends Value
{protected bytep90, p91, p92, p93, p94, p95, p96, p97,// ... (大量字节填充)p150, p151, p152, p153, p154, p155, p156, p157;
}// ... (其他代码)
public class Sequence extends RhsPadding
{// ... (类主体)
}
  • LhsPadding (Left-hand side Padding): 在 value 字段之前定义了56个字节的填充。
  • Value: 继承了 LhsPadding,并定义了核心的 protected long value; 字段(8字节)。
  • RhsPadding (Right-hand side Padding): 继承了 Value,并在 value 字段之后又定义了56个字节的填充。
  • Sequence: 最终的 Sequence 类继承自 RhsPadding

通过这种方式,value 字段在内存布局中被前后各56个字节的“保护区”包围起来,极大概率上保证了它自己独占一个64字节的缓存行,从而消除了伪共享带来的性能损耗。

核心设计二:使用 VarHandle 实现精细的并发控制

Sequence 的另一个核心是它如何处理并发读写和内存可见性。你可能会注意到,value 字段本身并没有被声明为 volatile。这是因为 Disruptor 需要比 volatile 更精细、开销更低的内存控制。它通过 java.lang.invoke.VarHandle 来实现这一点。

VarHandle 是 Java 9 引入的 API,可以看作是更安全、更标准的 sun.misc.Unsafe 的替代品,用于在变量级别上进行原子或有序操作。

// ... (其他代码)
public class Sequence extends RhsPadding
{static final long INITIAL_VALUE = -1L;private static final VarHandle VALUE_FIELD;static{try{VALUE_FIELD = MethodHandles.lookup().in(Sequence.class).findVarHandle(Sequence.class, "value", long.class);}catch (final Exception e){throw new RuntimeException(e);}}
// ... (其他代码)

在静态代码块中,Sequence 获取了父类 Value 中 value 字段的 VarHandle。之后的所有并发操作都通过这个 VALUE_FIELD 句柄来完成。

让我们分析一下 Sequence 的主要方法,看看 VarHandle 是如何被使用的。

构造函数

// ... (其他代码)public Sequence(final long initialValue){VarHandle.releaseFence();this.value = initialValue;}
// ... (其他代码)

构造函数中使用 VarHandle.releaseFence()。这是一个内存屏障,确保了对 initialValue 的赋值操作不会被重排序到屏障之后,并且之前的所有写入对其他线程可见。这保证了 Sequence 对象构造完成后,其初始值能被其他线程正确地观察到。

get() - 有序读(Acquire 语义)

// ... (其他代码)public long get(){long value = this.value;VarHandle.acquireFence();return value;}
// ... (其他代码)

这里的 get() 不仅仅是返回 this.value。它在读取值之后放置了一个 acquireFence()。这是一个 "load" 屏障,它确保了在此屏障之后的任何读写操作,都不会被重排序到屏障之前。

acquireFence 放在读取操作之后,是因为它的核心职责是防止后续的内存操作被提前执行,从而保证消费者在确认了“信号”(读取了新的 sequence 值)之后,再去安全地访问该信号所保护的“数据”(RingBuffer 中的事件)。它保护的是 get() 调用之后的代码逻辑。

set() - 有序写(Release 语义)

// ... (其他代码)public void set(final long value){VarHandle.releaseFence();this.value = value;}
// ... (其他代码)

set() 方法在写入值之前放置了一个 releaseFence()。这是一个 "store" 屏障,确保了在此屏障之前的任何读写操作,都不会被重排序到屏障之后。

这个顺序保证了以下几点:

  • 保证之前的操作不被重排到后面:当一个生产者线程调用 set(N) 之前,它肯定已经把数据写入了 RingBuffer 的第 N 个槽位。releaseFence 的作用就是确保“写数据到 RingBuffer”这个操作,绝对不会被编译器或 CPU 重排序到“更新 this.value”这个操作之后。
  • 保证内存可见性:releaseFence 会将当前线程工作内存中的所有修改刷新到主内存中。这样,当 this.value 的新值被写入主内存时,之前对 RingBuffer 槽位的修改也一定已经刷新到主内存了。

如何与 get() 共同建立 Happens-Before 关系?

set 和 get 的 happens-before 关系不是通过它们直接调用对方建立的,而是通过对同一个共享变量 value 的读写操作,并由内存屏障来保证顺序而建立的。

让我们把生产者和消费者的完整流程串起来:

生产者线程 (Producer):

  1. event.setData("some data"); // 操作A: 写入数据到 RingBuffer 的某个槽位

  2. sequence.set(N); // 调用 set 方法

    • VarHandle.releaseFence(); // 屏障B: 保证操作A一定在屏障B之前发生
    • this.value = N; // 操作C: 更新序号值

消费者线程 (Consumer):

  1. long currentSeq = sequence.get(); // 调用 get 方法

    • long value = this.value; // 操作D: 读取到序号值 N
    • VarHandle.acquireFence(); // 屏障E: 保证屏障E在操作D之后发生
  2. event = ringBuffer.get(currentSeq); // 操作F: 根据序号 N 去 RingBuffer 读取数据

Happens-Before 链条的建立:

  • 根据代码顺序,操作A happens-before 屏障B。
  • 根据 releaseFence 的语义,屏障B happens-before 操作C。
  • 当消费者在操作D中读到了生产者在操作C中写入的值 N 时,就建立了一个同步关系,意味着操作C happens-before 操作D。
  • 根据代码顺序,操作D happens-before 屏障E。
  • 根据 acquireFence 的语义,屏障E happens-before 操作F。

把它们串起来,我们就得到了一个完整的链条: 操作A (写数据) -> ... -> 操作C (写序号) -> 操作D (读序号) -> ... -> 操作F (读数据)

这个链条严格保证了:如果消费者线程看到了更新后的序号 N,那么它也一定能看到生产者在更新序号之前写入到 RingBuffer 中的数据。

VarHandle.releaseFence() 放在 this.value = value 之前,是为了保护它之前的所有操作,确保这些操作的结果对其他线程可见,然后再“发布”sequence 的新值。

可以把它想象成一个发布公告的流程:

  1. 你必须先把公告的内容(数据)完完整整地写好。
  2. 然后设置一个 releaseFence,这相当于一个规定:“必须写完内容才能去贴公告”。
  3. 最后,你把公告(sequence 的值)贴到公告栏上。

如果顺序反了,就可能发生你只贴了一个标题(更新了 sequence),但内容(数据)还没写完的混乱情况。

compareAndSet() - 原子比较并设置

// ... (其他代码)public boolean compareAndSet(final long expectedValue, final long newValue){return VALUE_FIELD.compareAndSet(this, expectedValue, newValue);}
// ... (其他代码)

这是标准的 CAS (Compare-And-Set) 操作,利用 VarHandle 提供的原子性保证。如果当前值等于 expectedValue,就原子地更新为 newValue 并返回 true,否则返回 false。这是实现无锁数据结构的基础。

addAndGet() / getAndAdd() - 原子加

// ... (其他代码)public long addAndGet(final long increment){return (long) VALUE_FIELD.getAndAdd(this, increment) + increment;}public long getAndAdd(final long increment){return (long) VALUE_FIELD.getAndAdd(this, increment);}
// ... (其他代码)

这两个方法提供了原子加法操作。VALUE_FIELD.getAndAdd() 会原子地将 increment 加到 value 上,并返回增加的旧值。

  • addAndGet 需要返回新值,所以它在 getAndAdd 的结果上又加了一次 increment
  • getAndAdd 直接返回 getAndAdd 的结果(旧值)。

在 Disruptor 中的应用

Sequence 是 Disruptor 协调机制的基石:

  1. Sequencer 的游标 (Cursor): 在 AbstractSequencer 中,有一个 cursor 字段 (protected final Sequence cursor = new Sequence(...)),它记录了生产者当前发布到的最大序号。生产者在申请下一批事件槽时会更新这个 cursor
  2. EventProcessor 的进度: 每个消费者(EventProcessor)都有自己的 Sequence,用于记录自己消费到的事件序号。
  3. Gating Sequences (门控序列)Sequencer 会追踪所有直接消费者的 Sequence。生产者在发布新事件时,需要确保不会覆盖掉最慢的消费者还未处理的事件。这个最慢的消费者的 Sequence 值就成了“门”,即 Gating Sequence。FixedSequenceGroup 就是一个典型的例子,它继承了 Sequence,但它的 get() 方法返回的是一组 Sequence 中的最小值。

总结

Sequence 类是一个看似简单但设计极其精巧的并发组件。它通过缓存行填充解决了硬件层面的伪共享问题,又通过 VarHandle 实现了比 volatile 更灵活、更低开销的内存可见性与原子性保证。正是这些底层的极致优化,共同构成了 Disruptor 高性能的基石。

http://www.lryc.cn/news/606159.html

相关文章:

  • 去重、top_n()、pull()、格式化
  • 数据结构第4问:什么是栈?
  • BR/EDR PHY帧结构及其具体内容
  • 51c自动驾驶~合集12
  • python基础语法3,组合数据类型(简单易上手的python语法教学)(课后习题)
  • 从0到1了解热部署
  • 一天两道力扣(7)
  • 力扣 hot100 Day61
  • 银河麒麟桌面操作系统:自定义截图快捷键操作指南
  • 机器人学和自动化领域中的路径规划方法
  • 解决Git升级后出现的问题
  • 国产芯+单北斗防爆终端:W5-D防爆智能手机,助力工业安全通信升级
  • 将开发的软件安装到手机:环境配置、android studio设置、命令行操作
  • ClickHouse vs PostgreSQL:数据分析领域的王者之争,谁更胜一筹?
  • 2683. 相邻值的按位异或
  • USRP捕获手机/路由器数据传输信号波形(中)
  • DeepCompare文件深度对比软件:智能差异分析与可视化功能深度解析
  • visual studio 安装总结
  • 搭建文件共享服务器samba————附带详细步骤
  • Kubernetes (K8s) 部署Doris
  • Redis过期策略
  • 【嵌入式电机控制#23】BLDC:开环运动控制框架
  • 设计模式:命令模式 Command
  • 法国声学智慧 ,音响品牌SK (SINGKING AUDIO) 重构专业音频边界
  • Web开发-PHP应用原生语法全局变量数据接受身份验证变量覆盖任意上传(代码审计案例)
  • HighgoDB查询慢SQL和阻塞SQL
  • 电商项目_性能优化_高并发缓存一致性
  • 当过滤条件不符合最左前缀时,如何有效利用索引? | OceanBase SQL 优化实践
  • 0731 IO进程基础
  • FATFS文件系统