谈谈 Netty 的无锁化设计?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
架构理解深度:面试官想看的是你有没有真正理解 Netty 的 Reactor 线程模型,能不能说清楚为什么单线程加串行化反而比加锁快。光知道个 "无锁化" 的名词根本不够看。
-
细节掌握程度:考察你是否清楚
inEventLoop()这个方法的作用、MpscQueue是什么、Channel与EventLoop的绑定关系,这些都是判断你是不是真读过 Netty 的硬指标。 -
实践认知:看你能不能说清楚无锁化的边界。Netty 不是真的全程无锁,跨线程场景下还是得靠 MPSC 队列和 volatile 保驾护航,盲目认为 "Netty 全程无锁" 容易露馅。
核心答案
Netty 的 "无锁化设计" 并不是真的完全不加锁,它的核心思路是用串行化让大部分场景压根不产生竞争。归纳起来就三句话:
- 一个
Channel整个生命周期只由一个EventLoop负责处理,所有 IO 操作和 Pipeline 事件传播都在这个 EventLoop 对应的唯一线程上执行 - 单线程内部当然不需要加锁,因为根本不存在并发
- 当外部线程要往 EventLoop 提交任务时,Netty 用
MpscQueue(多生产者单消费者无锁队列)+inEventLoop()判断来避免锁竞争
| 场景 | 是否加锁 | 实现方式 |
|---|---|---|
| 同一个 EventLoop 内的操作 | ❌ 不加锁 | 单线程天然无并发 |
| 外部线程提交任务给 EventLoop | ❌ 不加锁 | MpscQueue 无锁队列 |
Channel 的事件传播 |
❌ 不加锁 | Pipeline 在 EventLoop 内串行执行 |
EventLoopGroup 内部选择 EventLoop |
❌ 不加锁 | EventExecutorChooserFactory 取模或位运算 |
深度解析
一、串行化设计的核心思想
先用一张图看清楚 Netty 的线程模型:
这张图把 Netty 的核心设计思想画得挺清楚的,几个关键点拎出来看:
- 绑定关系是固定的:一个
Channel从注册到EventLoop开始,整个生命周期内所有操作都由这同一个EventLoop处理,不会切换线程 - EventLoop 是单线程的:一个
EventLoop内部只跑一个线程,绑定到它的所有Channel都共享这一个线程 - 没有共享就没有竞争:所有对某个
Channel的读写、Pipeline 事件传播都在同一个线程内完成,加锁就变得多余了
这一点其实是 Netty 借鉴了 Disruptor 的 "单写者原则"(Single Writer Principle)。只要保证同一时刻只有一个线程在写某个数据结构,那这个写操作就不需要任何同步。
二、inEventLoop():判断要不要切线程
光有绑定关系还不够,外部线程随时可能调用 Channel 的方法。比如业务线程池里跑着的代码,经常会主动 ctx.writeAndFlush()。这时候 Netty 怎么保证线程安全?
答案就是 AbstractChannelHandlerContext#inEventLoop() 这个判断:
// 简化版伪代码,展示核心逻辑
public ChannelFuture writeAndFlush(Object msg) {
// 判断当前调用线程是不是 EventLoop 线程
if (executor.inEventLoop()) {
// 在 EventLoop 线程内:直接执行,无锁
pipeline.writeAndFlush(msg);
} else {
// 不在 EventLoop 线程:把任务封装成 Runnable,
// 丢进 EventLoop 的任务队列,让它自己串行执行
executor.execute(() -> pipeline.writeAndFlush(msg));
}
return promise;
}
这一段代码特别关键,几乎所有的 Netty 线程安全都建立在这个判断上。重点拎一下:
- 在 EventLoop 线程内:直接执行,没有任何同步开销
- 不在 EventLoop 线程:把要做的事打包成一个 task,扔进 EventLoop 的 MPSC 队列排队,让 EventLoop 自己串行处理
- 本质上还是串行化:不管谁调用,最终所有对 Channel 的操作都会跑到那一个 EventLoop 线程里串行执行,所以不需要锁
三、MpscQueue:跨线程提交任务的无锁队列
当外部线程要提交任务时,多个生产者往同一个 EventLoop 队列里塞东西,单消费者(EventLoop 线程)往外取。这种 "多生产者单消费者" 场景,Netty 用的是 MpscQueue(Multi-Producer Single-Consumer Queue),基于 CAS 实现的无锁队列。补充一句,Netty 底层其实是 shaded 了 JCTools 库的 MpscArrayQueue 实现,不是从零造轮子。
这张图描述的就是多生产者单消费者队列的工作方式:
- 入队(多生产者):多个线程通过 CAS 操作竞争设置链表尾节点,避免了
synchronized或ReentrantLock的重锁开销 - 出队(单消费者):只有一个 EventLoop 线程在取,天然无竞争
- 比阻塞队列更轻:相比
LinkedBlockingQueue那种双锁设计,MpscQueue入队只需要 CAS,出队无锁,性能高得多
Netty 之所以要自己搞一套队列而不用 JDK 原生的,是因为 LinkedBlockingQueue 在多生产者场景下锁竞争非常严重,而 ConcurrentLinkedQueue 又是 MPSM(多生产者多消费者)的,对单消费者场景做了不必要的开销。
四、ChannelPipeline 的串行传播
// ChannelPipeline 的事件传播也是无锁的
// 以 fireChannelRead 为例
public ChannelHandlerContext fireChannelRead(Object msg) {
// 找到下一个 handler
AbstractChannelHandlerContext next = findContextInbound();
// 直接调用,不加任何锁
// 因为整个 Pipeline 都在同一个 EventLoop 线程内执行
next.invokeChannelRead(msg);
return this;
}
Pipeline 的设计本身就是单向链表 + 串行执行。所有 ChannelHandler 都在同一个 EventLoop 线程里被调用,整个事件传播链上没有任何 synchronized 或 Lock。
这块当年坑过不少人:ChannelHandler 不是线程安全的,但因为它只在 EventLoop 单线程里被调用,所以也不需要线程安全。如果你自己写了个 Handler,里面用了外部共享变量,那 Netty 的无锁化设计救不了你,这个锅得你自己背。
五、EventExecutorChooserFactory:EventLoop 分配也是无锁的
还有一个容易被忽略的点:当一个新的 Channel 注册到 EventLoopGroup 时,怎么选一个 EventLoop?
// DefaultEventExecutorChooserFactory 的实现
// 性能优化到极致——连取模都用位运算替代
public final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
@Override
public EventExecutor next(EventExecutor[] executors) {
// 位运算,比 % 快
return executors[idx.getAndIncrement() & executors.length - 1];
}
}
// 如果不是 2 的幂次,用普通取模
public final class GenericEventExecutorChooser implements EventExecutorChooser {
@Override
public EventExecutor next(EventExecutor[] executors) {
return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}
}
idx 是 AtomicLong,用 CAS 自增,无锁分配。Netty 在这里还做了个小优化:如果 EventLoopGroup 的大小是 2 的幂次,就用位运算代替取模(& (n-1) 等价于 % n),连这点性能都抠出来了。
面试高频追问
- Netty 是完全无锁的吗?
不是。"无锁化" 是指对热点路径(IO 读写、事件传播、任务分发)通过串行化避免锁竞争,但底层依然会用到 CAS(如 AtomicLong、MpscQueue)和 volatile。准确说是 "尽量无锁,必须同步时用 CAS"。
- 为什么 Netty 不用
synchronized而要用串行化?
synchronized 在高并发下会有上下文切换、内核态切换这些开销,缓存也容易失效。串行化把所有对同一 Channel 的操作都收敛到单线程,根本不产生竞争,性能比加锁好得多。
- 业务线程频繁
writeAndFlush会不会有性能问题?
会有额外开销,每次 writeAndFlush 都会创建一个 Runnable 对象扔进 MpscQueue,增加 GC 压力。生产环境建议批量提交或者做聚合,避免高频跨线程调用。
- 如果 Handler 里要执行耗时操作怎么办?
必须把耗时操作丢到业务线程池里,不能阻塞 EventLoop。一旦 EventLoop 被卡住,绑定在它上面的所有 Channel 都会跟着卡死。这是 Netty 使用中最常见的坑。
常见面试变体
- "Netty 是怎么保证线程安全的?"
- "
ChannelHandler需要做成线程安全的吗?为什么?" - "Netty 的
EventLoop和 Java 的EventLoop有什么区别?" - "为什么 Netty 要自己实现一个
MpscQueue?" - "Netty 中如何避免锁竞争?"
记忆口诀
"一绑二串三判断":
- 一绑:一个
Channel绑定一个EventLoop - 二串:单线程串行执行,没有共享
- 三判断:
inEventLoop()切线程,外部任务丢队列
总结
Netty 的无锁化其实没什么魔法,就是把 "串行化避免竞争" 这个思路用到极致。核心就一句话:把对同一资源的所有操作收敛到同一个线程里串行执行,没有共享自然就不需要锁。MpscQueue 和 inEventLoop() 这两个细节补完了最后一块拼图。面试只要把 "串行化 + inEventLoop + MpscQueue" 这三个关键词串起来讲清楚,基本就稳了。
