本节摘要:Future 是"取结果的单据",Promise 是"填结果的单据"。Netty 的 ChannelFuture 扩展了 JDK Future 的监听能力,Promise 则让任意异步操作都能挂进同一套体系。本节讲清两者的关系、addListener 组合写法,以及"谁在哪个线程收货"的判断方法。
第一章写过 b.connect(...).sync()——sync 会阻塞等到连接结果。但 EventLoop 线程里绝不能这么等(6.1 的自等死锁),所以要换监听风格:
ChannelFuture cf = b.connect("127.0.0.1", 8080); cf.addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { System.out.println("连接成功:" + future.channel()); future.channel().writeAndFlush("hello\n"); } else { System.err.println("连接失败:" + future.cause().getMessage()); } }); // 主线程立刻往下走,不等
这里的 ChannelFuture 就是一张单据:connect 这个异步操作开工时给你开一张,结果出来前单据是"未完成"态,出来后变"成功"或"失败",监听器在状态翻转的那一刻被回调——回调发生在哪个线程?答案是与单据关联的 EventLoop 线程(对 connect 来说是客户端 EventLoop)。
JDK 的 Future 有两个先天不足:get 只能阻塞轮询,且 cancel 之外没有完成通知。Netty 的扩展正中要害:
| 能力 | JDK Future | Netty ChannelFuture |
|---|---|---|
| 阻塞获取 | get(抛检查异常) | sync / await(区分抛与不抛) |
| 完成通知 | 无 | addListener 回调 |
| 失败原因 | get 时抛出 | cause 直接可查 |
| 关联对象 | 无 | channel 一并持有 |
Future 是只读单据,Promise 是它的可写面——Netty 里 ChannelPromise 既是出站操作的回执,也能拿来包装任意异步操作:
// 把"查数据库"包装成 Netty 风格的异步单据 public Future<User> fetchUserAsync(long uid, EventExecutor executor) { Promise<User> promise = new DefaultPromise<>(executor); bizExecutor.execute(() -> { try { User u = db.queryUser(uid); // 慢活 promise.setSuccess(u); // 填单:成功 } catch (Exception e) { promise.setFailure(e); // 填单:失败 } }); return promise; // 单据立刻可发,先用后填 } fetchUserAsync(9527, ctx.executor()).addListener(f -> { if (f.isSuccess()) { ctx.writeAndFlush(((Future<User>) f).getNow().getName()); // 注意:getNow 不阻塞,单据已完成才有值 } else { ctx.close(); } });
setSuccess/setFailure 只能调一次,第二次调抛 IllegalStateException——单据不能改口。这个"一次性"正是 Promise 语义的核心:完成即定局,监听器要么已通知要么即将通知,绝无竞态。
监听器里再发起异步操作,一层套一层就是"回调地狱"。压平的办法是把每步都变成单据传递,用 Netty 的 GlobalEventExecutor(或业务线程池)串起来:
// 目标:登录 → 拉资料 → 预扣额度,三步全异步且不嵌套 Promise<Session> loginPromise = loginAsync(userId, password, ctx.executor()); loginPromise.addListener((FutureListener<Session>) f -> { Session s = f.getNow(); fetchUserAsync(s.getUid(), ctx.executor()) .addListener((FutureListener<User>) fu -> { reserveAsync(fu.getNow(), ctx.executor()) .addListener(rf -> ctx.writeAndFlush(rf.isSuccess() ? "OK" : "FAIL")); }); });
嵌套仍有一层,但每层都是"单据进、单据出",没有阻塞、没有自等。更深的组合建议引入 CompletableFuture 做胶水:NettyFuture 与 CompletableFuture 之间用 listener 一行互转,后者提供 thenCompose 的扁平组合能力——两套体系各取所长。
writeAndFlush 返回的也是 ChannelFuture,出站操作完成(写进系统发送缓冲)时触发。两类典型用法:
// 用法一:写完就关——短连接回包的标准收尾 ctx.writeAndFlush(resp).addListener(ChannelFutureListener.CLOSE); // 用法二:确认发出后做统计/重试 ctx.writeAndFlush(msg).addListener(f -> { if (!f.isSuccess()) { log.warn("发送失败 准备重连 {}", f.cause().getMessage()); f.channel().close(); } });
注意"写完成"指字节进了内核发送缓冲,不等于对方收到——需要应用层 ACK 时,协议要自带确认消息,传输层单据回答不了这个问题。
⚠️ 常见坑三连:在 EventLoop 线程里 sync/await 自家单据(死锁,6.1 讲过);listener 里再调 get() 阻塞(listener 已在完成态,getNow 才是正确姿势);Promise 忘了 setFailure,调用方单据永远悬挂——每个异步分支的异常出口都必须填单。
💡 关键直觉:把异步操作都想成"开单—干活—填单"三步:调用方拿单先行,干活的线程填单定局,监听器收单续作。判断任何 Future 代码的正确性,就看填单路径是否覆盖了所有出口(成功、失败、超时)。