SpmcArrayQueue 是 JCTools 中为 单生产者-多消费者(Single-Producer-Multi-Consumer) 场景设计的有界队列。与 SPSC 模型相比,SPMC 的复杂性主要体现在消费者侧,因为多个消费者线程需要以线程安全的方式竞争消费同一个队列中的元素。

单生产者-单消费者数组队列 分析见:JCTools Spsc:单生产者-单消费者无锁队列

SpmcArrayQueue 的继承链同样是为了精确控制内存布局,但其侧重点与 SpscArrayQueue 有所不同,它需要处理多消费者对 consumerIndex 的争用。

  1. SpmcArrayQueueL1Pad & SpmcArrayQueueProducerIndexField:

    • 与 SPSC 类似,这部分定义了生产者索引 producerIndex,并用 L1Pad 将其与上游的“冷”字段(如 buffermask)隔离开。
    • 由于只有一个生产者,producerIndex 的更新逻辑相对简单,不需要 CAS 操作,使用 putOrderedLong 即可。
  2. SpmcArrayQueueL2Pad & SpmcArrayQueueConsumerIndexField:

    • 核心变化点SpmcArrayQueueConsumerIndexField 中不再有 soConsumerIndex 方法,取而代之的是 casConsumerIndex
       
      // ... existing code ...
      //$gen:ordered-fields
      abstract class SpmcArrayQueueConsumerIndexField<E> extends SpmcArrayQueueL2Pad<E>
      {protected final static long C_INDEX_OFFSET = fieldOffset(SpmcArrayQueueConsumerIndexField.class, "consumerIndex");private volatile long consumerIndex;// ... existing code ...@Overridepublic final long lvConsumerIndex(){return consumerIndex;}final boolean casConsumerIndex(long expect, long newValue){return UNSAFE.compareAndSwapLong(this, C_INDEX_OFFSET, expect, newValue);}
      }
      // ... existing code ...
      
    • 原因:因为有多个消费者,它们必须通过 CAS(Compare-And-Swap) 操作来原子性地更新 consumerIndex,以确保只有一个消费者能成功获取并消费一个元素。L2Pad 在此的作用依然是隔离生产者和消费者的热点字段。
  3. SpmcArrayQueueMidPad & SpmcArrayQueueProducerIndexCacheField:

    • SPMC 特有的优化:这里引入了一个新的字段 producerIndexCache
      // ... existing code ...
      //$gen:ordered-fields
      abstract class SpmcArrayQueueProducerIndexCacheField<E> extends SpmcArrayQueueMidPad<E>
      {// This is separated from the consumerIndex which will be highly contended in the hope that this value spends most// of it's time in a cache line that is Shared(and rarely invalidated)private volatile long producerIndexCache;// ... existing code ...
      }
      // ... existing code ...
      
    • producerIndex 会被生产者频繁更新(每次 offer 都会更新)
    • producerIndexCache 只在消费者发现缓存过期时才更新,因此减少了对 producerIndex 的 volatile 读取,降低了缓存一致性流量的争用
    • MidPad 的作用就是将这个消费者侧的缓存producerIndexCache)与消费者侧的争用点consumerIndex)分离开,避免它们互相干扰。
  4. SpmcArrayQueueL3Pad: 最后的填充,隔离 producerIndexCache 和 SpmcArrayQueue 自身的字段。


offer(E e):简单而直接

由于只有一个生产者,offer 的逻辑比 SPSC 还要简单,因为它不需要“前瞻优化”(producerLimit)。生产者只需要检查目标槽位是否为空即可。

// ... existing code ...@Overridepublic boolean offer(final E e){if (null == e){throw new NullPointerException();}final E[] buffer = this.buffer;final long mask = this.mask;final long currProducerIndex = lvProducerIndex(); // 1. 获取当前生产者索引final long offset = calcCircularRefElementOffset(currProducerIndex, mask);// 2. 检查槽位是否被消费者释放if (null != lvRefElement(buffer, offset)){// 如果槽位不为空,说明队列满了long size = currProducerIndex - lvConsumerIndex();if (size > mask){return false;}else{// 等待消费者释放该槽位 (这会破坏无等待性)while (null != lvRefElement(buffer, offset)){// BURN}}}// 3. 放置元素并更新索引soRefElement(buffer, offset, e);soProducerIndex(currProducerIndex + 1); // 使用 store-ordered 更新return true;}
// ... existing code ...

SPMC 特性分析

  1. 单生产者权威:生产者是唯一能推进 producerIndex 的线程,所以它只需 lvProducerIndex() 读取自己的进度,然后用 soProducerIndex() 更新即可,无需 CAS。
  2. 依赖消费者:生产者通过 lvRefElement 检查目标槽位是否为 null 来判断队列是否已满。这个 null 是由消费者在消费后写入的。
  3. 潜在的自旋等待:代码中有一段 while 循环等待。这通常发生在消费者进度稍稍落后于生产者进度,但队列并未完全满的情况下。生产者会在此“自旋”等待消费者完成对该槽位的消费和清理。这是 SpmcArrayQueue 的一个关键特性,它牺牲了一定的无等待性(Wait-Free)来简化设计

poll():竞争与缓存的艺术

poll 方法是 SPMC 模型的核心,完美展现了多消费者如何通过 CAS 和本地缓存来协同工作。

// ... existing code ...@Overridepublic E poll(){long currentConsumerIndex;// 1. 读取本地的生产者进度缓存long currProducerIndexCache = lvProducerIndexCache();do{// 2. 读取全局的消费者进度currentConsumerIndex = lvConsumerIndex();// 3. 快路径判断:使用本地缓存判断队列是否为空if (currentConsumerIndex >= currProducerIndexCache){// 4. 慢路径:本地缓存表明队列为空,需同步最新的生产者进度long currProducerIndex = lvProducerIndex();if (currentConsumerIndex >= currProducerIndex){return null; // 队列确实为空}else{// 更新本地缓存currProducerIndexCache = currProducerIndex;svProducerIndexCache(currProducerIndex);}}}// 5. CAS 竞争:尝试原子性地将 consumerIndex 加一while (!casConsumerIndex(currentConsumerIndex, currentConsumerIndex + 1));// 6. 成功获取元素return removeElement(buffer, currentConsumerIndex, mask);}
// ... existing code ...

SPMC 特性分析

  1. 生产者进度缓存:每个消费者线程开始时都会读取 producerIndexCache。这是一个本地快照,避免了每次都去访问真正的 producerIndex
  2. 快慢路径分离
    • 快路径:只要 currentConsumerIndex < currProducerIndexCache,消费者就认为队列中有元素,直接进入第5步的 CAS 竞争。这是绝大多数情况。
    • 慢路径:当快路径条件不满足时,消费者必须通过 lvProducerIndex() 读取最新的生产者进度,并更新自己的本地缓存 producerIndexCache
  3. CAS 争用casConsumerIndex 是多消费者协调的核心。多个消费者线程可能同时读取到相同的 currentConsumerIndex,但只有一个能通过 CAS 操作成功地将其加一,从而“赢得”消费该位置元素的权利。失败的线程则会重新循环,读取新的 consumerIndex 再次尝试。
  4. 无竞争取出:一旦一个消费者通过 CAS 成功预定了位置,它就可以安全地调用 removeElement 来取出元素。因为 removeElement 操作的是一个已经被它“私有化”的索引,不会有其他消费者来干扰。

SpmcArrayQueue.peek() 方法详细分析

peek() 方法是队列中的一个重要操作,它允许查看队列头部元素但不移除该元素。

public E peek() {final E[] buffer = this.buffer;final long mask = this.mask;long currProducerIndexCache = lvProducerIndexCache();long currentConsumerIndex;long nextConsumerIndex = lvConsumerIndex();E e;do {currentConsumerIndex = nextConsumerIndex;if (currentConsumerIndex >= currProducerIndexCache) {long currProducerIndex = lvProducerIndex();if (currentConsumerIndex >= currProducerIndex) {return null;} else {currProducerIndexCache = currProducerIndex;svProducerIndexCache(currProducerIndex);}}e = lvRefElement(buffer, calcCircularRefElementOffset(currentConsumerIndex, mask));// sandwich the element load between 2 consumer index loadsnextConsumerIndex = lvConsumerIndex();} while (null == e || nextConsumerIndex != currentConsumerIndex);return e;
}

方法开始首先获取几个关键变量:

  • buffer: 队列的底层数组,存储实际元素
  • mask: 用于计算循环队列位置的掩码值,通常是 capacity-1
  • currProducerIndexCache: 生产者索引的缓存值,这是一个重要的优化变量
  • currentConsumerIndex 和 nextConsumerIndex: 消费者索引的当前值和下一个值

  1. 在 peek() 方法中,消费者首先检查 producerIndexCache
  2. 只有当 currentConsumerIndex >= currProducerIndexCache 时才需要读取真实的 producerIndex
  3. 大多数情况下,队列不为空时,消费者可以直接使用缓存值,避免读取主 producerIndex
  4. 性能影响

    • 直接读取 producerIndex:每次都要从主内存读取,可能触发缓存失效
    • 使用 producerIndexCache:大部分时间从本地缓存读取,减少内存屏障和缓存一致性流量
  5. 正确性保证

    • 虽然使用了缓存,但通过 lvProducerIndex() 的检查确保了最终一致性
    • 当缓存可能过期时(currentConsumerIndex >= currProducerIndexCache),会重新读取真实值

缓存更新逻辑

if (currentConsumerIndex >= currProducerIndexCache) {long currProducerIndex = lvProducerIndex();if (currentConsumerIndex >= currProducerIndex) {return null; // 队列为空} else {currProducerIndexCache = currProducerIndex;svProducerIndexCache(currProducerIndex); // 更新缓存}
}

这段代码处理了两种情况:

  1. 队列为空:当消费者索引已经赶上或超过生产者索引时,返回 null
  2. 缓存过期:当缓存值小于实际生产者索引时,更新缓存值

元素加载

e = lvRefElement(buffer, calcCircularRefElementOffset(currentConsumerIndex, mask));

这行代码从缓冲区中加载元素:

  • calcCircularRefElementOffset 计算循环队列中的实际位置
  • lvRefElement 是一个 volatile 加载操作,确保内存可见性

一致性检查

// sandwich the element load between 2 consumer index loads
nextConsumerIndex = lvConsumerIndex();
} while (null == e || nextConsumerIndex != currentConsumerIndex);

这是一个重要的并发控制机制,被称为"三明治加载":

  1. 在加载元素前获取消费者索引(currentConsumerIndex
  2. 加载元素本身
  3. 再次获取消费者索引(nextConsumerIndex

通过比较两次获取的消费者索引是否相同,可以确保在加载元素过程中没有其他消费者修改了队列状态。如果不同,说明有其他消费者已经修改了队列,需要重试。

循环条件分析

while (null == e || nextConsumerIndex != currentConsumerIndex)

循环继续的条件有两个:

  1. null == e: 加载的元素为 null,可能是因为:

    • 队列确实为空
    • 生产者正在写入元素但尚未完成
    • 其他消费者已经取走了该元素
  2. nextConsumerIndex != currentConsumerIndex: 消费者索引在加载元素过程中被修改,说明有并发操作干扰

内存访问顺序

方法中的内存访问遵循特定顺序,确保正确的并发语义:

  1. 首先读取生产者索引缓存(lvProducerIndexCache()
  2. 读取消费者索引(lvConsumerIndex()
  3. 如果需要,读取实际生产者索引(lvProducerIndex()
  4. 读取队列元素(lvRefElement()
  5. 再次读取消费者索引(lvConsumerIndex()

这种顺序确保了在多线程环境下能正确检测队列状态变化。

volatile 操作的作用

  • lvProducerIndexCache(): volatile 读取,确保获取最新的缓存值
  • lvConsumerIndex(): volatile 读取,确保获取最新的消费者位置
  • lvProducerIndex(): volatile 读取,确保获取最新的生产者位置
  • svProducerIndexCache(): volatile 写入,确保缓存更新对所有线程可见

这些 volatile 操作确保了多线程间的内存可见性,防止出现不一致的视图。

循环优化

虽然方法中包含一个 do-while 循环,但在正常情况下(队列不为空且没有并发干扰),循环只会执行一次。只有在以下情况下才会多次循环:

  1. 队列为空
  2. 有并发消费者干扰
  3. 生产者正在写入元素但尚未完成

与其他方法的比较

peek() vs poll()

  • peek() 只查看元素但不移除
  • poll() 查看并移除元素
  • peek() 不需要修改消费者索引,而 poll() 需要通过 CAS 操作更新消费者索引

peek() vs relaxedPeek()

@Override
public E relaxedPeek() {final E[] buffer = this.buffer;final long mask = this.mask;long currentConsumerIndex;long nextConsumerIndex = lvConsumerIndex();E e;do {currentConsumerIndex = nextConsumerIndex;e = lvRefElement(buffer, calcCircularRefElementOffset(currentConsumerIndex, mask));// sandwich the element load between 2 consumer index loadsnextConsumerIndex = lvConsumerIndex();}while (nextConsumerIndex != currentConsumerIndex);return e;
}

relaxedPeek() 是 peek() 的"宽松"版本:

  • 不检查队列是否为空
  • 不使用生产者索引缓存
  • 只确保在加载元素过程中消费者索引没有变化

这使得 relaxedPeek() 性能更好,但可能在某些边界情况下行为不同(例如当队列为空时)。

适用场景

peek() 方法特别适用于以下场景:

  1. 检查队列内容:在不修改队列状态的情况下查看头部元素
  2. 多消费者环境:在有多个消费者线程的情况下,确保正确处理并发访问
  3. 性能敏感场景:通过缓存机制减少对共享变量的访问,提高性能
  4. 需要强一致性保证:相比 relaxedPeek(),提供更强的一致性保证

 

总结 

SpmcArrayQueue 相比 SpscArrayQueue 的核心特性和设计权衡在于:

  • 多消费者协调:引入了 CAS 操作 (casConsumerIndex) 来解决多个消费者对 consumerIndex 的争用问题,这是从 SPSC 到 SPMC 的根本性变化。
  • 消费者侧缓存:增加了 producerIndexCache 字段,让每个消费者可以缓存生产者进度,大大减少了对 producerIndex 的 volatile 读,降低了缓存一致性流量,提升了消费者侧的性能。
  • 内存布局的进一步细分:通过 MidPad 将 consumerIndex(极热争用点)和 producerIndexCache(消费者本地缓存)进行隔离,进一步优化了缓存性能。
  • 性能权衡:在 offer 方法中,允许了短暂的自旋等待,牺牲了严格的无等待性,以换取更简单的生产者逻辑。

总而言之,SpmcArrayQueue 是一个通过精巧的内存布局、CAS竞争和消费者侧缓存机制,高效解决了单生产者、多消费者并发难题的优秀实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/94078.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/94078.shtml
英文地址,请注明出处:http://en.pswp.cn/web/94078.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringAI1.0.1实战教程:避坑指南25年8月最新版

Spring AI 1.0.1 使用教程 项目简介 作为一个Java的开发者 听到Java也有ai框架了 很高兴~~~ 本来想学一下SpringAI但是网上卖课的一大堆&#xff0c;并且大部分课程都是五月的&#xff0c;到2025年的8月份&#xff0c;SpringAI的版本做了很多更新&#xff0c;所以我本人参考…

Maven架构的依赖管理和项目构建

​​​​​​什么是依赖管理对第三方依赖包的管理&#xff0c;可以连接互联网下载项目所需第三方jar包。对自己开发的模块的管理&#xff0c;可以像引用第三方依赖包一样引用自己项目的依赖包。Maven的依赖管理方式和传统方式有什么区别传统方式&#xff1a;从官网手动下载jar包…

微信小程序开发(一):使用开发者工具创建天气预报项目

Hi&#xff0c;我是前端人类学&#xff08;之前叫布兰妮甜&#xff09;&#xff01; 从今天开始&#xff0c;我将开启一个全新的微信小程序开发系列教程&#xff0c;通过实际项目带大家系统学习小程序开发。作为系列的第一篇文章&#xff0c;我们将从最基础的环境搭建开始&…

【链表 - LeetCode】24. 两两交换链表中的节点

24. 两两交换链表中的节点 - 力扣&#xff08;LeetCode&#xff09; 题解&#xff1a; - 迭代 首先是直接遍历的做法&#xff0c;这里注意调整指针指向的顺序。 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* List…

爬虫基础学习-链接协议分析,熟悉相关函数

1、urlparse&#xff1a;&#xff08;python标准库中的一个模块&#xff0c;解析和操作url&#xff09;标准的url链接格式&#xff1a;scheme://netloc/path;params?query#fragmentscheme&#xff08;协议&#xff09; http or https netloc&#xff08;网络位置&#xff09; …

kkfileview预览Excel文件去掉左上角的跳转HTML预览、打印按钮

上篇说了使用nginx代理kkfile预览文件&#xff0c;但是又发现个新问题&#xff0c;预览其他文件时都正常&#xff0c;但是预览.xlsx格式的时候&#xff0c;在左上角会有【跳转HTML预览】【打印】两个按钮&#xff0c;如下所示&#xff1a;这篇就来说一下如何去掉。首先这个跟kk…

阿里开源新AI code工具:qoder功能介绍

下载地址&#xff1a; https://qoder.com/ 文档地址&#xff1a; https://docs.qoder.com/ 文章目录1. AI 编码发展趋势2. 真实世界软件开发的挑战3. 我们的方法3.1. 透明度3.1.1. 知识可见性3.1.2. 执行透明度3.2. 增强上下文工程3.3. 规范驱动与任务委托3.3.1. 聊天模式&…

什么是短视频矩阵系统企业立项功能源码开发,支持OEM

短视频矩阵系统企业立项功能源码开发解析在短视频行业蓬勃发展的当下&#xff0c;企业纷纷布局短视频矩阵&#xff0c;以实现多平台、多账号的协同运营。而企业立项作为短视频矩阵项目启动的关键环节&#xff0c;其高效、规范的管理直接影响项目的推进效率与成果。为此&#xf…

当GitHub宕机时,我们如何协作?

问题背景与影响 GitHub作为主流代码托管平台的依赖现状宕机对分布式团队、CI/CD流水线、紧急修复的影响案例其他类似平台&#xff08;GitLab、Bitbucket&#xff09;的潜在连带风险 本地与离线协作方案 利用Git分布式特性&#xff1a;本地仓库继续提交&#xff0c;恢复后同步搭…

【会议跟踪】Model-Based Systems Engineering (MBSE) in Practice 2025

会议主旨与议题 会议宣传链接:https://www.sei.cmu.edu/events/mbse-in-practice/ 本次会议将于2025年8月21日位美国弗吉尼亚州阿灵顿(五角大楼所在地)举行。本次会议主旨为 MBSE in Practice: Bridging the Gap Between Theory and Success(2025)。随着软件定义系统日趋…

浏览器的渲染流程:从 HTML 到屏幕显示

在我们日常使用浏览器浏览网页时&#xff0c;往往忽略了浏览器背后复杂的渲染过程。从输入 URL 到页面最终显示在屏幕上&#xff0c;浏览器需要经过一系列精心设计的步骤。 浏览器渲染的整体流程浏览器的渲染流程可以大致分为两个主要部分&#xff1a;网络 和 渲染。当用户在地…

FastMCP 客户端服务器通信示例:从入门到实战(STDIO 传输)

引言 在现代分布式系统和AI应用中&#xff0c;模型上下文协议&#xff08;MCP&#xff09;扮演着重要角色&#xff0c;它负责协调客户端与服务器之间的通信&#xff0c;尤其是在需要频繁交互的场景中。本文将介绍如何使用FastMCP库快速实现客户端与服务器之间的通信&#xff0c…

宝可梦肉鸽 PC/手机双端 多种存档 全闪光 无限金币 全宝可梦解锁 免安装中文版

网盘链接&#xff1a; 宝可梦肉鸽 免安装中文版 名称&#xff1a;宝可梦肉鸽 PC/手机双端 多种存档 全闪光 无限金币 全宝可梦解锁 免安装中文版 描述&#xff1a;宝可梦肉鸽修改版是一款非常受欢迎的口袋妖怪系列&#xff0c;游戏拥有许多独特的妖怪和玩法。在游戏中&#…

Linux 下的网络编程

1、目的实现不同主机上进程间的通信。2、问题主机与主机之间在物理层面必须互联互通。进程与进程在软件层面必须互联互通。IP地址&#xff1a;计算机的软件地址&#xff0c;用来标识计算机设备。MAC地址&#xff1a;计算机的硬件地址&#xff08;固定&#xff09;。网络的端口号…

Go语言在边缘计算中的网络编程实践:从入门到精通

一、引言 在数字化浪潮席卷全球的今天&#xff0c;边缘计算如同一股清流&#xff0c;正在重新定义我们对网络架构的理解。想象一下&#xff0c;当你在自动驾驶汽车中需要毫秒级响应&#xff0c;或者在偏远工厂中需要实时处理传感器数据时&#xff0c;传统的云计算模式就像是&qu…

ASPICE过程能力确定——度量框架

&#x1f697;【汽车人必看】ASPICE能力评估核心&#xff1a;度量框架全解析&#xff5c;90%工程师都搞不懂的评分规则&#xff01;&#x1f50d; 为什么你的ASPICE评估总卡在L2&#xff1f;——揭秘6大能力等级背后的评分逻辑&#xff0c;附提升秘籍&#xff01;&#x1f525;…

机器学习在量化中的应用

一、核心应用场景在因子研究中&#xff0c;scikit-learn 主要解决以下几类问题&#xff1a;因子预处理与标准化&#xff1a;StandardScaler, RobustScaler因子有效性分析&#xff1a;LinearRegression (IC分析)降维与因子合成&#xff1a;PCA, FactorAnalysis机器学习预测模型&…

RabbitMQ:消息转化器

目录一、基本概述二、如何处理一、基本概述 在RabbitMQ中&#xff0c;一般情况下传递字符串会被正常解析&#xff0c;如果传递的是一个Object类型或者是一个对象类型的时候&#xff0c;RabbitMQ会将其自动转化为字节码发送&#xff0c;这不利于我们的读取个解析。 二、如何处…

【Protues仿真】基于AT89C52单片机的LCD液晶显示屏显示控制

目录 1 LM016L液晶显示模块 1.1 基本参数 1.2 引脚定义 1.3硬件连接示例&#xff08;AT89C52&#xff09; 1.4 常用指令集&#xff08;HD44780 子集&#xff09; 1.5 常见问题与注意事项 1.8 结论 2 LM016L液晶显示模块控制电路原理图 3 LM016L液晶显示模块控制程序 …

孤独伤感视频素材哪里找?分享热门伤感短视频素材资源网站

你是不是也经常在抖音上刷到很火的伤感视频&#xff0c;那么伤感视频素材都在哪里可以下载呢&#xff1f;作为一名从业多年的视频剪辑师&#xff0c;今天就跟大家聊聊那些可以下载伤感素材高清无水印的网站&#xff0c;如果你也在苦苦找寻伤感素材&#xff0c;快来看看吧&#…