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

Sequence

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

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

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

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

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

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

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

java 复制代码
// ... (其他代码)
class LhsPadding
{
    protected byte
        p10, 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 byte
        p90, 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 的替代品,用于在变量级别上进行原子或有序操作。

java 复制代码
// ... (其他代码)
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 获取了父类 Valuevalue 字段的 VarHandle。之后的所有并发操作都通过这个 VALUE_FIELD 句柄来完成。

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

构造函数

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

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

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

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

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

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

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

java 复制代码
// ... (其他代码)
    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 关系?

setget 的 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() - 原子比较并设置

java 复制代码
// ... (其他代码)
    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() - 原子加

java 复制代码
// ... (其他代码)
    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 高性能的基石。

相关推荐
上官浩仁2 小时前
springboot excel 表格入门与实战
java·spring boot·excel
Livingbody2 小时前
10分钟完成 ERNIE-4.5-21B-A3B-Thinking深度思考模型部署
后端
haogexiaole2 小时前
Dijkstra 算法
算法
Hello.Reader2 小时前
从零到一上手 Protocol Buffers用 C# 打造可演进的通讯录
java·linux·c#
树码小子3 小时前
Java网络初识(4):网络数据通信的基本流程 -- 封装
java·网络
稻草人想看远方3 小时前
GC垃圾回收
java·开发语言·jvm
胡萝卜的兔3 小时前
go 日志的分装和使用 Zap + lumberjack
开发语言·后端·golang
papership3 小时前
【入门级-算法-6、排序算法: 插入排序】
数据结构·算法·排序算法
HAH-HAH4 小时前
【蓝桥杯 2024 国 Java A】粉刷匠小蓝
c++·学习·数学·算法·职场和发展·蓝桥杯·组合数学
en-route4 小时前
如何在 Spring Boot 中指定不同的配置文件?
java·spring boot·后端