JCTools Spsc:单生产者-单消费者无锁队列
SpscArrayQueue
SpscArrayQueue
是 JCTools 中性能最高的有界队列之一,专为 单生产者-单消费者(Single-Producer-Single-Consumer) 场景设计。它的实现是无锁(Lock-Free)并且是无等待(Wait-Free)的,这意味着任何线程的操作都可以在有限的步骤内完成,不会因为其他线程的挂起或延迟而无限等待。
我们将从继承体系与内存布局、构造函数、核心方法offer
、核心方法poll
以及其他关键方法几个方面来逐一剖析。
SpscArrayQueue
的性能秘诀很大程度上隐藏在其精巧的继承结构中。这个结构并非为了代码复用,而是为了精确控制对象在内存中的字段布局,从而最大化地利用 CPU 缓存并消除伪共享(False Sharing)。
让我们自顶向下梳理这个继承链:
ConcurrentCircularArrayQueue<E>
- 职责:提供了所有循环数组队列的基础设施。
- 核心字段:
final E[] buffer
(存储元素的数组),final long mask
(用于快速计算数组索引的掩码)。
SpscArrayQueueColdField<E>
- 职责:定义“冷”字段(Cold Field),即初始化后很少或不再改变的字段。
- 核心字段:
final int lookAheadStep
。这是“前瞻优化”的关键参数,在构造时计算一次后便不再改变。将它放在继承链的早期,是为了让它远离后面频繁变化的“热”字段。
SpscArrayQueueL1Pad<E>
- 职责:一级缓存行填充(L1 Padding)。
- 核心字段:
byte b000, b001, ...
等大量无用字节。 - 目的:它的唯一作用就是在
lookAheadStep
和即将定义的生产者索引之间填充足够的字节(通常是128字节),确保它们位于不同的 CPU 缓存行中。
SpscArrayQueueProducerIndexFields<E>
- 职责:定义生产者所需的所有索引字段。
- 核心字段:
private volatile long producerIndex
: 生产者索引。volatile
修饰,确保其对消费者线程的可见性。这是生产者侧的热点字段。protected long producerLimit
: 生产者限制索引。这是一个普通的long
,作为消费者进度的本地缓存,是“前瞻优化”的核心。它只被生产者线程访问,因此无需volatile
。
SpscArrayQueueL2Pad<E>
- 职责:二级缓存行填充(L2 Padding)。
- 核心字段:同样是大量无用字节。
- 目的:这是最关键的填充。它在生产者索引(
producerIndex
)和即将定义的消费者索引(consumerIndex
)之间制造了巨大的鸿沟,从物理上隔离了生产者和消费者线程各自的热点字段,彻底根除了伪共享。
SpscArrayQueueConsumerIndexField<E>
- 职责:定义消费者所需的核心索引字段。
- 核心字段:
private volatile long consumerIndex
: 消费者索引。volatile
修饰,确保其对生产者线程的可见性。这是消费者侧的热点字段。
SpscArrayQueueL3Pad<E>
- 职责:三级缓存行填充(L3 Padding)。
- 目的:在消费者索引之后再次进行填充,确保
SpscArrayQueue
自身可能添加的任何字段都与消费者索引隔离。
SpscArrayQueue<E>
- 职责:最终的实现类,提供了
offer
、poll
等具体方法的逻辑。
- 职责:最终的实现类,提供了
小结:这个看似复杂的继承链,实际上是一个精密的内存布局蓝图,其核心思想就是用空间(Padding)换时间(避免缓存伪共享)。
构造函数分析
// ... existing code ...
public class SpscArrayQueue<E> extends SpscArrayQueueL3Pad<E>
{public SpscArrayQueue(final int capacity){super(Math.max(capacity, 4));}
// ... existing code ...
super(Math.max(capacity, 4));
- 构造函数将容量的最小值强制设为 4。
- 原因:这与“前瞻优化”有关。
lookAheadStep
通常被计算为capacity / 4
。如果容量小于4,lookAheadStep
就会是0,导致优化失效。因此,最小容量为4可以保证lookAheadStep
至少为1,让优化机制能够正常工作。
offer
:前瞻优化的艺术
offer
方法是 SpscArrayQueue
性能王冠上的明珠。
// ... existing code ...@Overridepublic boolean offer(final E e){if (null == e){throw new NullPointerException();}// 1. 将字段加载到本地变量,避免重复访问final E[] buffer = this.buffer;final long mask = this.mask;final long producerIndex = this.lpProducerIndex(); // 2. 使用 lp (load plain)// 3. 快车道(Fast Path)判断if (producerIndex >= producerLimit &&!offerSlowPath(buffer, mask, producerIndex)) // 4. 慢车道(Slow Path){return false;}final long offset = calcCircularRefElementOffset(producerIndex, mask);// 5. 写入元素和索引soRefElement(buffer, offset, e); // store orderedsoProducerIndex(producerIndex + 1); // store orderedreturn true;}private boolean offerSlowPath(final E[] buffer, final long mask, final long producerIndex){final int lookAheadStep = this.lookAheadStep;// 6. 前瞻检查if (null == lvRefElement(buffer,calcCircularRefElementOffset(producerIndex + lookAheadStep, mask))){// 7. 更新 producerLimitproducerLimit = producerIndex + lookAheadStep;}else{// 8. 精确检查final long offset = calcCircularRefElementOffset(producerIndex, mask);if (null != lvRefElement(buffer, offset)){return false;}}return true;}
// ... existing code ...
- 本地变量:将
buffer
,mask
,producerIndex
加载到栈上,避免在方法内部重复从内存(堆)中读取,提高效率。 lpProducerIndex()
:lp
代表load plain
。生产者线程自身访问自己的索引,不需要volatile
读的内存屏障开销,直接用最快的普通读。- 快车道:
producerIndex >= producerLimit
是核心判断。只要生产者索引还没达到缓存的消费者进度限制,就认为队列有空位,直接向下执行,这是绝大多数情况下offer
的执行路径。 - 慢车道:当快车道条件不满足时,进入
offerSlowPath
。这表示生产者需要更新它对消费者进度的认知了。 soRefElement
/soProducerIndex
:so
代表store ordered
。这是一个轻量级的写操作,它保证了“先写元素,再更新索引”这个顺序不会被指令重排打乱。这对于消费者能正确看到数据至关重要。- 前瞻检查:这是慢车道的精髓。它不直接去读取
volatile
的consumerIndex
,而是去“偷看”未来lookAheadStep
步之后的位置上的元素是否为null
。它使用lvRefElement
(load volatile
) 来确保能看到消费者线程将该位置设为null
的操作。 - 更新
producerLimit
:如果“偷看”的位置是null
,说明消费者已经走得很远了。生产者就可以大胆地将自己的producerLimit
向前推进lookAheadStep
步,然后返回快车道继续高速生产。 - 精确检查:如果“偷看”的位置不是
null
,说明队列可能满了。此时,才需要做一次精确检查,即检查当前producerIndex
指向的位置是否为空。如果当前位置也不为空,则队列确实满了,offer
失败。
小结:offer
方法通过 producerLimit
缓存和“前瞻”机制,将昂贵的 volatile
读操作(检查消费者进度)的频率降到最低,使得生产者在绝大部分时间里都能在无跨线程同步的“快车道”上运行。
poll()
相比 offer
,poll
的逻辑要简单得多,因为它不需要做任何优化,只需直接与生产者“交接”即可。
// ... existing code ...@Overridepublic E poll(){final long consumerIndex = this.lpConsumerIndex(); // 1. lp (load plain)final long offset = calcCircularRefElementOffset(consumerIndex, mask);final E[] buffer = this.buffer;final E e = lvRefElement(buffer, offset); // 2. lv (load volatile)if (null == e){return null;}// 3. 清理元素和更新索引soRefElement(buffer, offset, null); // store orderedsoConsumerIndex(consumerIndex + 1); // store orderedreturn e;}
// ... existing code ...
lpConsumerIndex()
:消费者读取自己的索引,同样使用最高效的普通读。lvRefElement()
:这是poll
方法的核心。它必须使用volatile
读 (load volatile
) 来读取数组元素,以确保能够看到生产者线程通过soRefElement
写入的最新数据。这是生产者和消费者之间数据传递的“握手点”。soRefElement
/soConsumerIndex
:与offer
类似,使用store ordered
保证“先将槽位置为null
,再更新消费者索引”的顺序。将槽位置为null
对于offer
方法的“前瞻”检查至关重要。
drain
vs. poll
循环
drain
的目标是从队列中批量取出元素。
如果只是简单地循环调用 poll()
:
// 伪代码:一个简单但低效的 drain 实现
for (int i = 0; i < limit; i++) {E e = poll();if (e == null) {return i;}c.accept(e);
}
这样做的问题在于:
- 方法调用开销:每次循环都会有一次
poll()
方法的调用开销。 - 重复的字段加载:
poll()
方法内部每次都会去加载consumerIndex
,mask
,buffer
等字段。 - 逻辑冗余:
poll()
内部的null
判断会在循环中重复执行。
SpscArrayQueue
的实际 drain
实现: 它将 poll
的核心逻辑 内联(inline) 并展开到了 drain
的循环体内部。
// ... existing code ...@Overridepublic int drain(final Consumer<E> c, final int limit){// ... 参数检查 ...// 1. 一次性加载所有需要的字段到本地变量final E[] buffer = this.buffer;final long mask = this.mask;final long consumerIndex = this.lpConsumerIndex();// 2. 直接在循环中操作for (int i = 0; i < limit; i++){final long index = consumerIndex + i;final long offset = calcCircularRefElementOffset(index, mask);// 3. 直接从 buffer 中 volatile 读取元素,绕过 poll()final E e = lvRefElement(buffer, offset);if (null == e){return i;}// 4. 直接操作 buffer 和索引,绕过 poll()soRefElement(buffer, offset, null);soConsumerIndex(index + 1); // ordered store -> atomic and ordered for size()c.accept(e);}return limit;}
// ... existing code ...
优化点分析:
- 避免方法调用:完全没有
poll()
的调用,消除了相关开销。 - 字段一次性加载:在循环开始前将
buffer
,mask
,consumerIndex
加载到栈上,循环体内直接使用,速度更快。 - 逻辑内联:
poll()
的核心逻辑——计算偏移量、读取元素、判断null、设置null、更新索引——被直接实现在了drain
的循环中,没有任何冗余。
所以,drain
是一个高度优化的 poll
批量版本,而不是它的简单循环。
fill
vs. offer
循环
fill
的优化更为精妙,因为它必须处理 SpscArrayQueue
最核心的 前瞻(Look-Ahead) 机制。
如果只是简单地循环调用 offer()
:
// 伪代码:一个简单但性能很差的 fill 实现
for (int i = 0; i < limit; i++) {if (!offer(s.get())) {return i;}
}
这样做的问题非常严重:
- 破坏前瞻优化:每次调用
offer
都会进行一次producerIndex >= producerLimit
的检查。如果队列接近满,可能会频繁进入offerSlowPath
,导致性能急剧下降。 - 高昂的慢路径成本:在慢路径中,每次循环都会进行一次
lvRefElement
的volatile
读,这本是应该尽量避免的。
SpscArrayQueue
的实际 fill
实现: 它将前瞻机制也应用到了批量操作中,实现了批量版本的快、慢路径。
// ... existing code ...@Overridepublic int fill(final Supplier<E> s, final int limit){// ... 参数检查 ...final E[] buffer = this.buffer;final long mask = this.mask;final int lookAheadStep = this.lookAheadStep;final long producerIndex = this.lpProducerIndex();for (int i = 0; i < limit; i++){final long index = producerIndex + i;// 1. 批量快路径:检查前方 `lookAheadStep` 距离的位置final long lookAheadElementOffset =calcCircularRefElementOffset(index + lookAheadStep, mask);if (null == lvRefElement(buffer, lookAheadElementOffset)){// 2. 如果安全,一次性写入 lookAheadStep 个元素int lookAheadLimit = Math.min(lookAheadStep, limit - i);for (int j = 0; j < lookAheadLimit; j++){final long offset = calcCircularRefElementOffset(index + j, mask);soRefElement(buffer, offset, s.get());soProducerIndex(index + j + 1);}// 3. 外层循环步进,跳过已经填充的元素i += lookAheadLimit - 1;}else // 4. 批量慢路径:如果前方不安全,则回退到一次检查一个{final long offset = calcCircularRefElementOffset(index, mask);if (null != lvRefElement(buffer, offset)){return i; // 队列已满}soRefElement(buffer, offset, s.get());soProducerIndex(index + 1);}}return limit;}
// ... existing code ...
优化点分析:
- 批量前瞻:
fill
的循环不是一次检查一个槽位,而是一次“向前看”lookAheadStep
个槽位。 - 批量快路径:如果前瞻检查通过,它就获得了连续写入
lookAheadStep
个元素的“许可”,然后在一个内部循环里快速完成填充,极大地提高了效率。 - 成本分摊:昂贵的
volatile
读(lvRefElement
)的成本被分摊到了lookAheadStep
个元素上,而不是每个元素都承担一次。 - 批量慢路径:只有当快路径走不通时,才退化为逐个检查的慢路径,逻辑与单次
offer
的慢路径类似。
fill
和 drain
体现了 JCTools 的设计哲学:针对不同的使用场景(单个操作 vs 批量操作)提供专门优化的路径,以压榨出极致的性能。
总结
SpscArrayQueue
是一个将计算机体系结构、Java内存模型和算法设计完美结合的典范。它的卓越性能源于以下几个关键设计:
- 精密的内存布局:通过多层继承和缓存行填充,彻底消除了伪共享。
- 前瞻性优化:在生产者侧引入
producerLimit
缓存,实现了“快车道”和“慢车道”逻辑,极大降低了跨线程volatile
读的频率。 - 细粒度的内存屏障:充分利用
Unsafe
提供的plain
、ordered
、volatile
等不同强度的内存操作,在保证并发正确性的前提下,将性能压榨到极致。
因此,SpscArrayQueue
成为了单生产者单消费者场景下近乎完美的队列解决方案。
SpscArrayQueueColdField
这个抽象类是 SpscArrayQueue
继承链中的一个重要组成部分
SpscArrayQueue...
:这是为 SPSC (Single-Producer, Single-Consumer) 单生产者单消费者数组队列服务的基类。...ColdField
: "Cold Field"(冷字段)是 JCTools 中的一个术语,特指那些在对象构建后值不再改变或者极少改变的字段。将这些字段聚合在一起,可以提高 CPU 缓存的效率。因为这些字段所在的缓存行(Cache Line)一旦被加载,就很少会因为写操作而失效,从而减少了不必要的缓存同步开销。在这个类中,lookAheadStep
就是一个典型的“冷字段”。extends ConcurrentCircularArrayQueue<E>
: 它继承自ConcurrentCircularArrayQueue
,这意味着它复用了 JCTools 中通用的循环数组队列的基础设施,例如buffer
数组、mask
掩码以及容量计算等。
这个类的核心是定义了 final int lookAheadStep;
字段,并于构造函数中对其进行初始化。
这个字段是 SpscArrayQueue
实现高性能的关键优化——“前瞻(Look-Ahead)”——的基础。
abstract class SpscArrayQueueColdField<E> extends ConcurrentCircularArrayQueue<E>
{final int lookAheadStep;SpscArrayQueueColdField(int capacity){super(capacity);int actualCapacity = capacity();lookAheadStep = SpscLookAheadUtil.computeLookAheadStep(actualCapacity);}}
为什么需要前瞻优化?
在生产者-消费者模型中,生产者需要知道队列是否已满,才能决定能否继续添加元素。最直接的判断方式是比较生产者索引 pIndex
和消费者索引 cIndex
。但在并发环境中,pIndex
由生产者线程更新,cIndex
由消费者线程更新,这意味着生产者要读取一个由其他线程写入的 volatile
变量 cIndex
,这个操作相对耗时。
SpscArrayQueue
通过“前瞻”机制,巧妙地避免了在每次 offer
操作时都去读取 cIndex
。
前瞻优化如何工作?
缓存消费者进度: 生产者维护一个本地的
producerLimit
字段,作为对消费者进度的缓存。只要pIndex < producerLimit
,生产者就认为队列有空间,可以非常快速地执行offer
(这被称为“快车道”)。触发“慢车道”: 当
pIndex
追上producerLimit
时,生产者不能再假设队列有空间。此时触发“慢车道”逻辑 (offerSlowPath
方法)。执行“前瞻”: 在慢车道中,生产者并不直接去读取
cIndex
,而是“向前看”lookAheadStep
个距离。它会检查buffer[pIndex + lookAheadStep]
这个位置的元素是否为null
。- 如果为
null
: 这说明消费者至少已经消费到了这个位置,证明队列中至少还有lookAheadStep
个空位。此时,生产者就可以安全地将自己的producerLimit
更新为pIndex + lookAheadStep
,然后返回“快车道”继续快速offer
。 - 如果不为
null
: 说明队列可能真的满了,此时才需要进行更精确的检查(检查buffer[pIndex]
是否为空)。
- 如果为
lookAheadStep
的计算: 这个“步长”由SpscLookAheadUtil.computeLookAheadStep(actualCapacity)
计算得出,通常是容量的四分之一。这是一个权衡:步长越大,触发慢车道的频率越低,但可能导致队列在未完全占满时就提前认为已满;步长越小,判断越精确,但触发慢车道的频率会更高。
总结
SpscArrayQueueColdField
的作用是:
- 作为
SpscArrayQueue
继承链的一部分,通过“冷字段”的组织方式优化内存布局。 - 引入并初始化
lookAheadStep
字段,为SpscArrayQueue
的核心性能优化——前瞻机制——奠定基础。
这个机制使得 SpscArrayQueue
在大多数情况下,其 offer
操作都无需进行跨线程的 volatile
读,从而实现了极高的吞吐量,是无锁队列设计中的一个经典优化。
SpscArrayQueueProducerIndexFields
这个抽象类专门负责管理生产者侧的索引。
...ProducerIndexFields
: 名称后缀清晰地指出了它的职责——定义和管理与生产者索引(Producer Index)相关的字段。extends SpscArrayQueueL1Pad<E>
: 它继承自SpscArrayQueueL1Pad
。这意味着它位于lookAheadStep
字段和生产者索引之间,插入了一段缓存行填充(Padding)。这个L1Pad
的作用是将lookAheadStep
这个“冷”字段与producerIndex
这个“热”字段分离开,避免它们位于同一个缓存行而引发不必要的缓存失效。
这个类定义了两个关键的生产者端字段:
// ... existing code ...
// $gen:ordered-fields
abstract class SpscArrayQueueProducerIndexFields<E> extends SpscArrayQueueL1Pad<E>
{private final static long P_INDEX_OFFSET = fieldOffset(SpscArrayQueueProducerIndexFields.class, "producerIndex");private volatile long producerIndex;protected long producerLimit;SpscArrayQueueProducerIndexFields(int capacity)
// ... existing code ...
private volatile long producerIndex;
- 这是真正的生产者索引。它记录了生产者下一个将要写入数据的位置。
- 它被声明为
volatile
,这是至关重要的。因为consumerIndex
(由消费者线程更新)需要读取这个值来判断队列是否为空。volatile
保证了生产者对producerIndex
的写入对消费者线程是立即可见的。 - 这个字段是**“热”字段**,因为生产者线程会频繁地更新它。
protected long producerLimit;
- 这是生产者侧对消费者进度的缓存,是实现“前瞻(Look-Ahead)”优化的关键。
- 它不是
volatile
的。这是一个非常重要的性能优化点。因为这个字段只由生产者线程自身读写,它不需要保证跨线程的可见性。生产者线程可以非常快速地访问这个普通的long
类型字段。 - 它的值代表了生产者在不检查消费者真实进度的情况下,可以安全生产到的索引上限。当
producerIndex
达到producerLimit
时,才会触发“慢车道”逻辑去更新这个producerLimit
。
这个类还提供了一套访问和更新 producerIndex
的方法,这些方法都利用了 sun.misc.Unsafe
来实现最高效的内存操作。
// ... existing code ...@Overridepublic final long lvProducerIndex(){return producerIndex;}final long lpProducerIndex(){return UNSAFE.getLong(this, P_INDEX_OFFSET);}final void soProducerIndex(final long newValue){UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, newValue);}
// ... existing code ...
lvProducerIndex()
:lv
是load volatile
的缩写。它提供了对producerIndex
的volatile
读。这通常是给消费者线程使用的,以获取生产者最新的进度。lpProducerIndex()
:lp
是load plain
的缩写。它提供了对producerIndex
的普通(非volatile
)读。这只在生产者线程内部使用,因为生产者自己知道自己的进度,不需要volatile
语义带来的额外开销。soProducerIndex(final long newValue)
:so
是store ordered
的缩写。它使用putOrderedLong
来更新producerIndex
。这是一个比putVolatileLong
更轻量级的写操作。它保证了在它之前的内存写入操作不会被重排序到它之后,并且最终会将新值刷新到主存,但它不保证其他线程能立即看到这个新值(即它是一个“延迟写”)。这对于 SPSC 场景是足够的,因为只有消费者需要看到这个值的变化,而消费者侧的volatile
读最终会确保看到这个更新。
总结
SpscArrayQueueProducerIndexFields
是 SpscArrayQueue
高性能设计的核心体现:
- 职责分离:将生产者相关的索引字段集中管理。
- 缓存行填充:通过继承
L1Pad
,将热点字段producerIndex
与其他字段隔离开,避免伪共享。 - 优化字段访问:
- 使用
volatile
的producerIndex
确保跨线程的可见性。 - 使用非
volatile
的producerLimit
作为线程本地缓存,实现“快车道”入队。
- 使用
- 精细化内存操作:利用
Unsafe
提供的plain
、ordered
、volatile
等不同强度的内存操作,在保证正确性的前提下,将性能压榨到极致。
SpscArrayQueueConsumerIndexField
这个类在 SpscArrayQueue
的继承结构中扮演着与 SpscArrayQueueProducerIndexFields
对称的角色,它专门负责管理消费者侧的索引。
...ConsumerIndexField
: 后缀清晰地表明了它的核心职责——定义和管理消费者索引(Consumer Index)相关的字段和方法。extends SpscArrayQueueL2Pad<E>
: 它继承自SpscArrayQueueL2Pad
。这个继承关系非常关键,它在producerIndex
相关的字段和consumerIndex
字段之间插入了一大段缓存行填充(Padding)。L2Pad
的目的就是确保生产者线程高频访问的producerIndex
和消费者线程高频访问的consumerIndex
绝对不会位于同一个缓存行上。这可以彻底消除伪共享(False Sharing),是保证 SPSC 队列极致性能的基础。
这个类只定义了一个核心字段:
// ... existing code ...
//$gen:ordered-fields
abstract class SpscArrayQueueConsumerIndexField<E> extends SpscArrayQueueL2Pad<E>
{private final static long C_INDEX_OFFSET = fieldOffset(SpscArrayQueueConsumerIndexField.class, "consumerIndex");private volatile long consumerIndex;SpscArrayQueueConsumerIndexField(int capacity)
// ... existing code ...
private volatile long consumerIndex;
- 这是真正的消费者索引,记录了消费者下一个将要读取数据的位置。
- 它被声明为
volatile
,这对于生产者来说是必需的。生产者需要通过读取这个值来判断队列是否已满(虽然在SpscArrayQueue
中通过“前瞻”优化避免了频繁读取,但在“慢车道”逻辑中依然需要)。volatile
保证了消费者对consumerIndex
的更新对生产者线程是可见的。 - 这个字段是消费者线程侧的**“热”字段**,会被消费者线程频繁更新。
与生产者侧类似,这个类也提供了一套基于 sun.misc.Unsafe
的高效访问方法:
// ... existing code ...public final long lvConsumerIndex(){return UNSAFE.getLongVolatile(this, C_INDEX_OFFSET);}final long lpConsumerIndex(){return UNSAFE.getLong(this, C_INDEX_OFFSET);}final void soConsumerIndex(final long newValue){UNSAFE.putOrderedLong(this, C_INDEX_OFFSET, newValue);}
}
// ... existing code ...
lvConsumerIndex()
:load volatile
的缩写。它提供了对consumerIndex
的volatile
读。这主要是给生产者线程在“慢车道”中使用的,用以获取消费者最新的、准确的进度。lpConsumerIndex()
:load plain
的缩写。它提供了对consumerIndex
的普通(非volatile
)读。这只在消费者线程内部使用,因为消费者自己知道自己的进度,不需要volatile
带来的额外内存屏障开销。soConsumerIndex(final long newValue)
:store ordered
的缩写。它使用putOrderedLong
来更新consumerIndex
。这是一个轻量级的“延迟写”,保证了在它之前的内存操作(比如将数组元素设为null
)不会被重排序到它之后。这对于 SPSC 场景是完全足够且高效的,因为只有生产者需要观察到这个值的变化。
总结
SpscArrayQueueConsumerIndexField
的设计哲学与 SpscArrayQueueProducerIndexFields
一脉相承,共同构成了 SpscArrayQueue
的高性能基石:
- 职责明确:专门管理消费者索引。
- 隔离热点:通过继承
L2Pad
,将生产者和消费者的核心热点字段(producerIndex
和consumerIndex
)用缓存行填充进行物理隔离,从根本上杜绝了伪共享问题。 - 精细化内存控制:针对不同的使用场景(线程内访问 vs. 跨线程访问),提供了不同内存语义强度(
plain
,ordered
,volatile
)的Unsafe
操作方法,在保证并发正确性的前提下,最大化地提升了单线程的执行效率。