谈谈 Netty 的无锁化设计?

#中间件#面试八股

谈谈 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 用数组下标换掉哈希探测,但依赖专用线程类型。
  • 无锁的前提是不阻塞,两者是同一枚硬币的两面。
笔记加载中…