深入剖析 Netty MpscLinkedQueue 无锁队列:NioEventLoop 任务队列的高性能实现原理

发布时间:2026/9/20 23:55:34
深入剖析 Netty MpscLinkedQueue 无锁队列:NioEventLoop 任务队列的高性能实现原理 文档教程知识库【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址https://gitcode.com/doocs/source-code-hunter点击查看免费下载导读MpscLinkedQueueMulti-Producer Single-Consumer Linked Queue多生产者单消费者无锁链表队列是 Netty 内部实现的高性能无锁队列它被用作核心线程 NioEventLoop 中任务队列 taskQueue 的底层实现是支撑 Netty「串行无锁化设计」的关键基础设施之一。本文基于 Netty 4.1.6 源码从队列的初始化、头尾结点维护、offer() 无锁写入原理、remove() 不支持的原因以及伪共享规避等实现细节出发完整拆解这条队列「多生产者并发写入不加锁」的精妙设计帮助读者掌握 CAS 无锁队列的通用设计范式并理解其在 NioEventLoop 组件 与 HashedWheelTimer 调度 中的真实应用。MpscLinkedQueue 是什么在 Netty 核心中的核心成员 NioEventLoop 中任务队列的实现 taskQueue 便是 MpscLinkedQueue。MpscLinkedQueue 是 Netty 实现的一个基于多生产者单消费者的无锁队列针对 NioEventLoop 中任务队列的特点单消费者的场景在一开始就避免了从队列中取数据时加锁的必要最精妙的地方在于多生产者并发从队列中添加数据的时候也没有加锁达到了 Netty 所期望的高性能实现。这种设计并非偶然。从 Netty 的整体设计看Netty 高性能之道 中明确阐述了其「无锁化的串行设计」思想消息的处理尽可能在同一个线程内完成期间不进行线程切换从而避免多线程竞争和同步锁带来的性能损耗。NioEventLoop 正是这个串行化设计的执行者——它作为 Reactor 线程既要轮询处理 I/O 事件又要执行外部线程提交过来的普通任务与定时任务而所有任务都汇聚到 taskQueue 这一条队列上。外部业务线程生产者可以同时向 NioEventLoop 提交任务而消费任务的只有 NioEventLoop 自身这一个线程消费者这恰好构成一个典型的 MPSC 场景。上图展示了 NioEventLoop 通过 ChannelPipeline 中的 Handler 链对消息进行串行、无锁处理的工作模型——所有处理都发生在单个 I/O 线程内部不进行线程切换这正是 MpscLinkedQueue 得以无锁化的场景前提。MpscLinkedQueue 无锁并发线程安全写入原理尾结点的维护继承 AtomicReference 带来的原子能力首先MpscLinkedQueue 继承自AtomicReference也就是说 MpscLinkedQueue 通过继承 AtomicReference 的方式显式地维护了一个提供原子读写能力的变量value。而在 MpscLinkedQueue 中这个value是其内部维护的队列的尾结点。继承 AtomicReference 并非随意之举而是整个无锁方案的核心基础value字段对所有线程可见任何线程都能读到最新的尾结点引用getAndSet()/compareAndSet()等方法底层由 UNSAFE 的 CAS 指令保证原子性使得「把新节点设为尾结点」这一步可以在多线程并发下安全地完成。头结点的维护构造方法与哨兵节点MpscLinkedQueue() { MpscLinkedQueueNodeE tombstone new DefaultNodeE(null); headRef new FullyPaddedReferenceMpscLinkedQueueNodeE(); headRef.set(tombstone); setTail(tombstone); }在 MpscLinkedQueue 中维护着headRef头结点字段其队列内部节点的实现是MpscLinkedQueueNode。MpscLinkedQueueNode 是一个除了存放具体队列元素外只有next字段的节点也就是说 MpscLinkedQueue 的队列是单向的。构造过程的关键点先创建了一个 value 为null的DefaultNode即哨兵节点 tombstone它同时充当初始的头结点和尾结点headRef通过FullyPaddedReference封装其作用详见后文「伪共享的规避」一节在构造方法的最后通过setTail()方法将 MpscLinkedQueue 的尾结点字段value也设置为这个哨兵节点。头结点字段headRef的存在可以方便后续直接从头结点开始的队列操作消费者可以简单判断头尾节点是否相等来确认队列中是否有元素可以消费——初始状态下 head tail队列为空。无锁加入的核心offer() 与 replaceTail()Override SuppressWarnings(unchecked) public boolean offer(E value) { if (value null) { throw new NullPointerException(value); } final MpscLinkedQueueNodeE newTail; if (value instanceof MpscLinkedQueueNode) { newTail (MpscLinkedQueueNodeE) value; newTail.setNext(null); } else { newTail new DefaultNodeE(value); } MpscLinkedQueueNodeE oldTail replaceTail(newTail); oldTail.setNext(newTail); return true; } private MpscLinkedQueueNodeE replaceTail(MpscLinkedQueueNodeE node) { return getAndSet(node); }MpscLinkedQueue 的offer()方法很简短但恰恰就是整个队列元素加入的完整流程当元素被加入的时候空值校验如果加入的元素为null直接抛出NullPointerException节点封装判断加入的元素是否本身就是一个MpscLinkedQueueNode如果不是则用DefaultNode进行封装如果本身就是节点则先将它的next置为null确保新节点是干净独立的原子替换尾结点通过replaceTail()方法将当前被加入的节点通过 AtomicReference 所提供的getAndSet()方法设为队列的尾结点并返回先前的尾结点。这次操作由 UNSAFE 的 CAS 来保证操作的原子性链接旧尾结点将之前的尾结点的next指向新加入的节点本次加入宣告结束。整个操作的核心逻辑可以概括为「先让新节点在原子层面抢占尾结点位置再让旧尾结点指向新节点」。其精妙之处在于getAndSet()的原子性保证了每个新节点只会被一个生产者成功设为尾结点不会有多个生产者同时抢到同一个尾结点位置当多个生产者并发执行时线程 A 拿到的oldTail与线程 B 拿到的oldTail必然是不同的节点因为尾结点位置已被 CAS 唯一确定因此各自执行oldTail.setNext(newTail)时写的是不同节点的next字段互不干扰唯一可能的顺序差异是线程 B 已经把自己的节点设为尾结点但线程 A 还没来得及执行oldTail.setNext(newTail)即「链接」这一步晚于「抢占」这一步。这种情况下新节点的入队顺序可能不完全按照提交先后排列但由于是链表实现不会产生数据丢失或覆盖只是在不加锁的前提下队列顺序可能不会严格按照加入顺序——这在 NioEventLoop 的任务场景下并不是问题。从这里可以看出MpscLinkedQueue 利用了 AtomicReference 底层 UNSAFE 的能力通过 CAS 确保新设置进入value的节点必定能够和原先的节点达成一个且唯一的联系那么只需要自顶向下不断通过将这个联系变成引用一条队列便形成了。由于其实现是链表而不是数组也就没有涉及到资源的竞争。在这个前提下高并发的插入场景下每个新进入的新节点都将获取原尾位置value上的节点而自身将会被设置为其后驱节点重新放到尾结点位置上CAS 在不加锁的前提下保证了前后节点对应关系的唯一性完成了并发条件下不加锁的线程安全写入。为什么 MpscLinkedQueue 不支持 remove()在 MpscLinkedQueue 中是不支持remove()方法去从队列中移除任意一个元素的。原因很简单消费者和生产者是无锁的消费者可以通过比较队首和队尾元素是否一致来保证线程安全地从队首取数据但是remove()从队列中任意位置修改数据是线程不安全的主要体现在移除队尾元素可能会导致正在加入的新元素被丢弃。具体来说如果某个线程要从队列中移除「当前尾结点」而此时另一个生产者线程正在执行offer()生产者拿到的oldTail可能正是这个即将被移除的节点。当remove()将该节点的next断开后生产者后续执行oldTail.setNext(newTail)就会把新节点挂到一个已从队列断开的节点上造成新加入的元素「游离」在队列之外而被永久丢弃。因此为了保证 MPSC 场景下写入的绝对安全Netty 选择了不支持任意位置删除操作。MpscLinkedQueue 另外的实现细节伪共享的规避FullyPaddedReference 的填充MpscLinkedQueue 中的头节点被通过FullyPaddedReference封装。其内部前后分别填充 56 字节和 64 字节来进行填充以避免伪共享False Sharing导致的性能损耗使得头结点可以被高效地访问。伪共享的背景CPU 缓存以缓存行通常为 64 字节为单位进行读写如果多个线程频繁访问的变量恰好落在同一条缓存行上那么任何一个线程对其中一个变量的写操作都会导致其他线程的缓存行失效从而引发不必要的缓存同步开销。头结点headRef是消费者线程频繁读写的热点字段而value尾结点是生产者线程频繁读写的热点字段通过字节填充将它们隔离在不同缓存行中可以显著降低并发访问时的缓存竞争。值得注意的是FullyPaddedReference前后分别填充 56 字节和 64 字节——其中 56 字节加上引用自身的 8 字节正好凑满一条 64 字节缓存行这正是对现代 CPU 缓存行大小64 字节的精确适配。UNSAFE 偏移量赋值内存屏障的优化MpscLinkedQueue 在消费者消费数据后当将下一个节点设置为头结点的时候并不是直接进行赋值而是通过 UNSAFE 来根据偏移量赋值。这样做将略微提高性能主要是内存屏障 storestore 和 loadstore 之间的性能差异直接引用赋值会被编译器/JVM 插入必要的内存屏障以保证可见性通过 UNSAFE 按偏移量写入时可以精确控制内存屏障的使用规避掉一些非必要的屏障开销从而获得微小的性能提升。这类细节优化虽然单次收益不大但在 Netty 这种对性能锱铢必较的高性能框架中任何一个热点路径上的微小优化都会被放大。延伸Mpsc 队列在 Netty 中的典型应用场景一NioEventLoop 的任务队列NioEventLoop 作为 Netty 的 Reactor 线程其线程模型与职责可参考 EventLoop 组件源码解析其内部维护的 taskQueue 正是 MpscLinkedQueue。外部线程通过eventLoop.execute(Runnable task)向 NioEventLoop 提交任务时任务会被加入到这条队列中而 NioEventLoop 自身的单一线程在每次事件循环中批量取出任务执行。由于消费者唯一取出时天然不需要加锁由于入队使用 CAS多生产者并发提交也无需加锁——这正是 NioEventLoop 能承载「无锁化串行设计」的底层保证。场景二HashedWheelTimer 的任务转移队列除了 NioEventLoopNetty 的时间轮定时器 HashedWheelTimer 也使用了 Mpsc 队列。在 HashedWheelTimer schedule 分析 中可以确认「将任务对象添加到 mpsc 队列中mpsc 是多生产者单消费者的队列模型另外 mpscQueue 是无锁队列靠 CAS 实现的」并且「schedule 方法其实也用到 MpscQueue只是任务执行的时候会把任务从 PriorityQueue 转移到 MpscQueue 上」。时间轮的场景同样是典型的 MPSC 结构多个业务线程可以并发地向时间轮提交定时任务多生产者而时间轮的 Worker 线程负责消费任务单消费者。这里复用与 NioEventLoop 相同的无锁队列方案进一步印证了 MpscLinkedQueue 在 Netty 内部「高性能并发任务收口」场景下的通用价值。总结MpscLinkedQueue 用不到一百行的核心代码实现了一条兼顾「多生产者并发写入安全」与「单消费者高效取出」的无锁队列其设计要点可以归纳为设计要点实现手段解决的问题原子尾结点继承 AtomicReference用value字段保存尾结点为多生产者并发写入提供 CAS 基础哨兵节点构造时创建DefaultNode(null)同时作为头尾结点简化空队列判断头尾相等即为空无锁入队replaceTail()即getAndSet()抢占尾结点再链接旧尾结点并发写入不加锁且不丢数据无 remove()设计上禁止任意位置删除避免移除尾结点导致新入队元素被丢弃伪共享规避FullyPaddedReference前后填充 56/64 字节隔离头尾热点字段的缓存行冲突内存屏障优化UNSAFE 按偏移量赋值头结点减少非必要内存屏障微调性能对于想在自己的高并发组件中设计无锁队列的开发者而言MpscLinkedQueue 提供了一套可复制的范式用 CAS 原子地确立「前后节点唯一对应关系」用链表规避数组扩容的资源竞争用单消费者假设省掉出队锁再用缓存行填充与内存屏障优化把性能压榨到极致。结合 Netty 串行无锁化设计 的整体思路阅读便能理解这条队列在 Netty 高性能体系中所扮演的关键角色。赞分享文档教程知识库【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址https://gitcode.com/doocs/source-code-hunter点击查看免费下载相关推荐Netty 源码剖析MpscLinkedQueue 无锁多生产者单消费者队列的实现原理Netty 源码剖析MpscLinkedQueue 无锁多生产者单消费者队列的实现原理 在 Netty 的核心组件 NioEventLoop 中任务队列 t文档教程技术博客知识库高性能C无锁队列ReaderWriterQueue高性能C无锁队列ReaderWriterQueue 项目介绍 ReaderWriterQueue 是一个为C设计的单生产者、单消费者SPSC无锁队并发编程终极指南Katana高性能消息队列如何实现异步任务处理终极指南Katana高性能消息队列如何实现异步任务处理 Katana作为下一代爬虫和蜘蛛框架其核心优势之一在于内置的高性能消息队列系统能够高效处理异步任务网络安全网页爬虫上一篇探秘开源游戏宝藏Awesome Unity Games下一篇zimfw 模块化架构揭秘从新手到专家的完整学习路径创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询