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