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 高性能的基石。

相关推荐
自由鬼1 小时前
如何处理Y2K38问题
java·运维·服务器·程序人生·安全·操作系统
淮北4941 小时前
STL学习(十一、常用的算数算法和集合算法)
c++·vscode·学习·算法
糖葫芦君1 小时前
玻尔兹曼分布与玻尔兹曼探索
人工智能·算法·机器学习
摸鱼仙人~1 小时前
Redis 数据结构全景解析
数据结构·数据库·redis
_oP_i4 小时前
RabbitMQ 队列配置设置 RabbitMQ 消息监听器的并发消费者数量java
java·rabbitmq·java-rabbitmq
Monkey-旭4 小时前
Android Bitmap 完全指南:从基础到高级优化
android·java·人工智能·计算机视觉·kotlin·位图·bitmap
我爱996!4 小时前
SpringMVC——响应
java·服务器·前端
小宋10215 小时前
多线程向设备发送数据
java·spring·多线程
大佐不会说日语~6 小时前
Redis高频问题全解析
java·数据库·redis
寒水馨6 小时前
Java 17 新特性解析与代码示例
java·开发语言·jdk17·新特性·java17