6.2 Handler 线程安全与上下文传递


6.2 Handler 线程安全与上下文传递

本节摘要:Handler 的线程安全性取决于它有没有"跨回调的可变状态"以及被几个线程调用。本节给出状态安放的三层选择(无状态、Channel 级、全局级)、@Sharable 的判定纪律,以及业务线程池整合时保序的标准姿势。

一、先判定:你的 Handler 有没有状态

写 Handler 前先问一句:不同回调之间要不要记东西?答案决定了全部并发策略:

// 形态一:纯无状态 —— 天然线程安全,可加 Sharable 注解做全局单例 @ChannelHandler.Sharable public class MetricsHandler extends ChannelInboundHandlerAdapter { private final LongAdder inBytes = new LongAdder(); // 只增不减的统计量,本身并发安全 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof ByteBuf b) inBytes.addLong(b.readableBytes()); ctx.fireChannelRead(msg); } } // 形态二:连接级状态 —— 放成员变量,但绝不加 Sharable(每连接 new 一个) public class LoginHandler extends ChannelInboundHandlerAdapter { private int retryTimes = 0; // 只属于这一条连接 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (!checkOk(msg) && ++retryTimes >= 3) ctx.close(); else ctx.fireChannelRead(msg); } }

@ChannelHandler.Sharable 的判定纪律一句话:只要存在非线程安全的成员变量,就绝不能共享。第三章说过每个连接会各装配一次 Pipeline,非共享 Handler 的成员变量天然是"连接私有"的;一旦加了 Sharable,同一成员被几十个线程读写,重则数据错乱、轻则概率性诡异 bug——这类问题的排查成本远高于省下的那点内存。

二、状态安放的三层选择

状态类型 安放位置 并发前提 典型例子
处理逻辑本身无状态 Handler 成员全 final 可 @Sharable 编解码、指标统计
单连接会话状态 非 Sharable Handler 成员 / Channel attr 单线程访问,免锁 登录重试计数、半包暂存
跨连接共享状态 外部 ConcurrentHashMap / 数据库 自行保证并发安全 在线用户表、全局限流器

第二层的细节:Channel 的 attr(3.1 讲过的属性袋)与非 Sharable Handler 成员都可以放会话状态,取舍是生命周期——attr 跟着 Channel 走,Handler 成员跟着工序走(流水线热插拔后,老 Handler 的成员就丢了)。会话 ID、鉴权结果这种"贯穿全程"的放 attr;握手中间态这种"工序局部"的放 Handler 成员。

第三层的标准写法:

// 全局在线表:跨线程共享,必须用并发容器 public class SessionRegistry { private static final ConcurrentHashMap<String, Channel> ONLINE = new ConcurrentHashMap<>(); public static void login(String userId, Channel ch) { Channel old = ONLINE.put(userId, ch); if (old != null && old != ch) old.close(); // 踢旧会话 ch.closeFuture().addListener(f -> ONLINE.remove(userId, ch)); // 断开自动清 } public static Channel find(String userId) { return ONLINE.get(userId); } }

注意 remove(userId, ch) 的双参版本:防止新会话顶号后,旧连接关闭回调又误删新会话——两值删除是并发容器的标准防误删姿势。

三、慢业务剥离:业务线程池的标准整合

Handler 里直接查库、调下游是卡死流水线的头号原因。标准解法是业务线程池 + 回写,但有个隐蔽要求:同一条连接的消息顺序不能乱。把两条消息扔进普通线程池并行跑,先发的可能后到。保证顺序有两种姿势:

// 姿势一:串行执行器——同连接的任务排队,不同连接并行(推荐,够用且简单) public class OrderedBizHandler extends SimpleChannelInboundHandler<MyMessage> { private static final Map<Channel, Executor> EXECUTORS = new ConcurrentHashMap<>(); @Override protected void channelRead0(ChannelHandlerContext ctx, MyMessage msg) { Executor ex = EXECUTORS.computeIfAbsent(ctx.channel(), c -> Executors.newSingleThreadExecutor()); // 每连接一个单线程队列 ex.execute(() -> { String result = slowRpcCall(msg); // 慢活在业务线程 ctx.writeAndFlush(result); // write 自动归一回 IO 线程 }); } @Override public void channelInactive(ChannelHandlerContext ctx) { Executor ex = EXECUTORS.remove(ctx.channel()); if (ex instanceof ExecutorService es) es.shutdown(); // 连接断,回收线程 } }
// 姿势二:无序并行 + 应用层序号重排——吞吐优先,实现复杂 // 响应带请求序号,发送前缓存乱序结果,缺口补齐后按序 flush。 // 只有在单连接消息互相独立且客户端能接受乱序时才考虑。

绝大多数业务用姿势一即可:每连接一个单线程队列,天然保序,线程数与在线连接数同量级(空闲线程很轻)。真正的海量连接场景,改用哈希取模的有界线程组:executors[Math.abs(chid % N)],连接间均匀分摊又不至于线程爆炸。

⚠️ 常见坑:业务线程里抛了未捕获异常——线程池把它吞掉,日志可能只有一行晦涩的告警,连接却卡在"等回包"。业务任务的 try-catch 要兜底并回写错误响应或关闭连接,别让异常无声无息。

四、上下文传递:ThreadLocal 的坑与 FastThreadLocal

Java 惯用的 ThreadLocal 在 Netty 里有个陷阱:同一个 EventLoop 线程服务多条 Channel,你 set 的"请求上下文"会被下一条 Channel 的请求覆盖——ThreadLocal 的边界是线程,而 Netty 的并发边界是 Channel。两种正确做法:

// 做法一:显式传参 / attr,不依赖线程(首选) ctx.channel().attr(REQ_CTX).set(requestContext); // 做法二:确实需要 ThreadLocal 语义(如埋点 traceId),用完立刻清理 FastThreadLocal<String> traceId = new FastThreadLocal<>(); traceId.set("t-123"); try { handle(msg); } finally { traceId.remove(); } // 不 remove 必串号

Netty 自家的 FastThreadLocal 用数组下标代替哈希探查,速度快一截,但语义坑与普通 ThreadLocal 相同:set 与 remove 必须成对,EventLoop 线程是复用的

💡 关键直觉:并发问题先问两个"谁":这段状态属于谁(Channel 还是全局)?这段代码跑在谁的线程上(EventLoop 还是业务池)?两个问题都有答案,方案就自然浮出来。

本节要点回顾

  • 三形态判定:无状态可共享、连接级绝不共享、全局级上并发容器。
  • @Sharable 纪律:有可变成员就不共享,共享的必须是纯函数式工位。
  • attr 与成员的取舍:生命周期跟着谁走,状态就放谁那里。
  • 慢业务剥离:每连接单线程队列保序,write 自动归一回 IO 线程。
  • 双参 remove:并发容器防误删的标准姿势(顶号踢人场景)。
  • ThreadLocal 边界错配:Netty 并发边界是 Channel 不是线程,优先显式传参。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U