谈谈 Netty 的无锁化设计?
结论先行:Netty 的无锁不是“没有并发”,而是“把并发消灭在结构里”。核心手法是串行化:一个 Channel 只由一个 EventLoop 线程处理,所有状态变更都在该线程内完成;跨线程通信则通过 MPSC 队列投递任务。再配合
FastThreadLocal、CAS 原子更新、引用计数,使热路径上几乎不出现锁。
一、四类无锁手法
| 手法 | 实现 | 消除的竞争 |
|---|---|---|
| 串行化 | Channel 绑定唯一 EventLoop | 同一连接的读写状态竞争 |
| 任务队列 | MpscQueue + taskQueue | 跨线程投递的锁 |
| 线程本地 | FastThreadLocal(数组下标寻址) | ThreadLocal 的 hash 冲突与清理开销 |
| 原子操作 | AtomicIntegerFieldUpdater、CAS | 引用计数、状态位的锁 |
| 分片 | 按 Channel 哈希选 EventLoop | 全局队列竞争 |
二、串行化:无锁的根本
Channel A ──┐
Channel B ──┼──> EventLoop-1(单线程)──> 顺序处理,无并发
Channel C ──┘
Channel D ──┐
Channel E ──┼──> EventLoop-2(单线程)
Channel F ──┘
结论:同一 Channel 的状态只被一个线程修改,天然线程安全
代价是:EventLoop 里任何一次阻塞,都会让它管理的所有 Channel 一起卡住。这是“无锁”的对价。
三、跨线程投递走 MPSC
// 业务线程想把消息写回 Channel
channel.writeAndFlush(msg);
// 内部逻辑(简化)
if (eventLoop.inEventLoop()) {
// 已经在本线程,直接执行,零开销
write(msg);
} else {
// 跨线程:封装成任务投递到 MPSC 队列,由 EventLoop 稍后执行
eventLoop.execute(() -> write(msg));
}
MpscQueue(JCTools 提供)是“多生产者单消费者”队列,只有一个消费者线程,因此出队无需加锁,入队用 CAS,比 LinkedBlockingQueue 快数倍。
四、FastThreadLocal 的寻址
ThreadLocal: threadLocalHashCode → 开放寻址探测 → 可能冲突
FastThreadLocal:构造时分配唯一 index → 直接数组下标访问
FastThreadLocalThread.internalThreadLocalMap
index: 0 1 2 3
[v0] [v1] [v2] [v3]
private static final FastThreadLocal<Session> SESSION = new FastThreadLocal<Session>() {
@Override
protected Session initialValue() { return new Session(); }
};
public void channelRead0(ChannelHandlerContext ctx, Msg msg) {
Session s = SESSION.get(); // 数组寻址,无哈希、无锁
s.touch();
}
注意:FastThreadLocal 只有跑在 FastThreadLocalThread 上才有加速效果,普通线程会退化为普通 ThreadLocal。
五、引用计数的原子更新
// AbstractReferenceCountedByteBuf 内部
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
public ByteBuf retain() {
for (;;) {
int old = refCnt;
if (old <= 0) throw new IllegalReferenceCountException(old);
if (refCntUpdater.compareAndSet(this, old, old + 1)) return this;
}
}
因为大多数 retain/release 发生在 EventLoop 线程内,CAS 几乎总是一次成功,不会真的进入自旋。
常见追问
- 追问:完全不用锁吗?不是。
Recycler的跨线程回收、PoolArena的共享状态仍有轻量同步,只是热路径上避开了。 - 追问:串行化会不会浪费多核?不会。EventLoop 数量默认 2×CPU,连接按哈希分散到多个线程,核是用满的。
- 追问:怎么判断代码是否破坏了无锁假设?在 Handler 里打日志看线程名,若同一 Channel 的回调出现在不同线程,说明有地方绕过了 EventLoop。
小结
- 无锁的核心是“串行化 + 分片”,不是消灭所有同步原语。
- 跨线程写通过 MPSC 队列转交,
inEventLoop()判断是零开销快路径。 FastThreadLocal用数组下标换掉哈希探测,但依赖专用线程类型。- 无锁的前提是不阻塞,两者是同一枚硬币的两面。