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 获取了父类 Value 中 value 字段的 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 关系?
set 和 get 的 happens-before 关系不是通过它们直接调用对方建立的,而是通过对同一个共享变量 value 的读写操作,并由内存屏障来保证顺序而建立的。
让我们把生产者和消费者的完整流程串起来:
生产者线程 (Producer):
-
event.setData("some data");// 操作A: 写入数据到 RingBuffer 的某个槽位 -
sequence.set(N);// 调用 set 方法VarHandle.releaseFence();// 屏障B: 保证操作A一定在屏障B之前发生this.value = N;// 操作C: 更新序号值
消费者线程 (Consumer):
-
long currentSeq = sequence.get();// 调用 get 方法long value = this.value;// 操作D: 读取到序号值 NVarHandle.acquireFence();// 屏障E: 保证屏障E在操作D之后发生
-
event = ringBuffer.get(currentSeq);// 操作F: 根据序号 N 去 RingBuffer 读取数据
Happens-Before 链条的建立:
- 根据代码顺序,操作A
happens-before屏障B。 - 根据
releaseFence的语义,屏障Bhappens-before操作C。 - 当消费者在操作D中读到了生产者在操作C中写入的值
N时,就建立了一个同步关系,意味着操作Chappens-before操作D。 - 根据代码顺序,操作D
happens-before屏障E。 - 根据
acquireFence的语义,屏障Ehappens-before操作F。
把它们串起来,我们就得到了一个完整的链条: 操作A (写数据) -> ... -> 操作C (写序号) -> 操作D (读序号) -> ... -> 操作F (读数据)
这个链条严格保证了:如果消费者线程看到了更新后的序号 N,那么它也一定能看到生产者在更新序号之前写入到 RingBuffer 中的数据。
VarHandle.releaseFence() 放在 this.value = value 之前 ,是为了保护它之前的所有操作 ,确保这些操作的结果对其他线程可见,然后再"发布"sequence 的新值。
可以把它想象成一个发布公告的流程:
- 你必须先把公告的内容(数据)完完整整地写好。
- 然后设置一个
releaseFence,这相当于一个规定:"必须写完内容才能去贴公告"。 - 最后,你把公告(
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 协调机制的基石:
- Sequencer 的游标 (Cursor) : 在
AbstractSequencer中,有一个cursor字段 (protected final Sequence cursor = new Sequence(...)),它记录了生产者当前发布到的最大序号。生产者在申请下一批事件槽时会更新这个cursor。 - EventProcessor 的进度 : 每个消费者(
EventProcessor)都有自己的Sequence,用于记录自己消费到的事件序号。 - Gating Sequences (门控序列) :
Sequencer会追踪所有直接消费者的Sequence。生产者在发布新事件时,需要确保不会覆盖掉最慢的消费者还未处理的事件。这个最慢的消费者的Sequence值就成了"门",即 Gating Sequence。FixedSequenceGroup就是一个典型的例子,它继承了Sequence,但它的get()方法返回的是一组Sequence中的最小值。
总结
Sequence 类是一个看似简单但设计极其精巧的并发组件。它通过缓存行填充 解决了硬件层面的伪共享问题,又通过 VarHandle 实现了比 volatile 更灵活、更低开销的内存可见性与原子性保证。正是这些底层的极致优化,共同构成了 Disruptor 高性能的基石。