了解异步的出现,可以先看 什么叫 IO就绪
- 异步、异步库为什么会出现?(你现在想问的:根源,在 Reactive Streams 之前很久就存在)
- Reactive Streams 规范为什么出现?(刚才我讲的:异步库多了、各自一套,缺统一标准、缺标准化背压)
一句话前置:异步不是为了背压而生;背压,是异步普及之后,暴露出来的一个棘手副作用问题。
下面从头梳理:为什么人类会搞异步、异步库。
一、最原始:同步模型的瓶颈(同步阻塞)
传统 Java 服务模型:一个请求绑定一个线程(Tomcat BIO 模型)
线程1:接收请求 → 调用JDBC(阻塞,等DB返回)→ 返回响应
当线程卡在 DB、RPC、文件读写这种IO等待的时候:
✅ CPU 是空闲的
❌ 线程被占死,不能处理别的请求
线程是昂贵资源:
- JVM 线程占用栈内存(默认 1MB 左右)
- 操作系统内核调度线程有开销
- 线程数量不能无限开,开到几千之后,上下文切换爆炸、OS 负载飙升
核心矛盾:IO 等待浪费线程资源。
同步模型里,等待IO=占着线程摸鱼。
在流量上涨的时候,很快线程池耗尽,新请求排队、超时,服务扛不住。
👉 解决方案思路:不要让线程卡在IO等待上。IO等待的时候,线程释放,去干别的活。
这个思路,就是异步。
二、异步的底层基础:操作系统提供的能力
异步不是Java发明的,是操作系统内核能力:
- Linux:select / poll / epoll
- Windows:IOCP
也就是常说的 非阻塞IO、IO多路复用
原理:
线程不再死等IO结果,而是告诉内核:帮我盯着这个socket,数据来了通知我。
线程立刻返回,可以去处理别的连接、别的任务。
等内核检测到数据就绪,再回调回来处理。
这是异步最底层来源:榨干IO等待间隙,用少量线程支撑大量IO连接。
典型场景:网关、IM、长连接、高并发HTTP服务,成千上万连接,但大部分时间都在等待。
同步:线程等IO(线程绑定IO)
异步:IO就绪了再通知线程(IO就绪才占用线程)
三、早期异步编程形态,以及痛点(异步库诞生的原因)
有了操作系统非阻塞IO之后,语言/框架要封装它,于是出现各种异步方案:
1. 原生回调(Callback)
伪代码:
java
readFile(path, result -> {
// IO完成之后才进来
sendHttp(result, resp -> {
saveDb(resp, () -> {
// 嵌套,回调地狱
})
})
})
优点:线程不阻塞,资源利用率高。
缺点:
- 回调地狱,嵌套很深,可读性崩盘
- 异常传递很难:内层异常怎么传到最外层?
- 取消、超时、重试、组合多个IO,代码极难写
- 顺序、并发、分支编排非常麻烦
2. 然后就出现了第一代异步库:为了简化回调
不同团队各自封装:
- Guava ListenableFuture
- Java 8 CompletableFuture(JDK官方异步)
- Netty(底层epoll,回调式异步网络库)
- 早期RxJava 1.x(为了流式事件编排)
这些异步库诞生的目的:
把零散的回调封装起来,提供链式编排、异常传递、组合能力,解决回调地狱。
⚠️ 注意:这一批异步库,目标是解决「回调难写、IO线程资源浪费」,根本没有考虑背压。
早期异步推送模型,大多是纯 Push:上游有数据就拼命往下发。
当遇到上游生产 > 下游消费,数据无处安放,只能堆在无界队列 → OOM。
这个坑,是异步流行之后,才慢慢大面积暴露出来的。
举个例子:Netty 读一个大文件流,上游源源不断读出字节,下游处理慢。如果没有限流,缓冲区无限涨,直接OOM。
四、时间线重新排一遍(非常关键,纠正之前顺序)
- 硬件 & OS 层面:存在IO等待问题
网络、磁盘IO远慢于CPU,同步线程模型浪费资源。 - 操作系统提供 epoll / IOCP 非阻塞多路复用 → 异步能力底座
- 出现原生回调写法,但是难写、难维护。
- 各种异步库雨后春笋出现(Guava、Netty、RxJava1)
✅ 目标:简化异步回调、高效利用少量线程承载大量IO
❌ 此时:没有统一标准,很多库没有背压机制 - 大规模落地后,暴露出新问题:异步推送容易OOM(生产者快于消费者)
各家自己写限流、队列、丢弃策略,实现五花八门,库与库之间流不能互通。 - → 于是才催生了 Reactive Streams 规范(2015)
目标两件事:- 定义一套统一接口,不同异步流式库可以互通
- 内置标准化背压协议(request(n)),解决异步push模型下的OOM问题
重点一句话区分两层目标:
✅ 异步、异步库诞生:为了解决IO等待浪费线程,提升并发承载能力,同时解决回调地狱。
✅ Reactive Streams 规范诞生:异步库已经很多了,为了统一契约、标准化背压。
五、再对应回你一直在学的 Reactor / Mono/Flux
Project Reactor 本身就是一个异步编排库,建立在 Reactive Streams 规范之上:
- 它首先是异步库:用来高效处理大量IO(HTTP、DB、消息),用少量线程扛高并发,规避BIO线程浪费;
- 其次,它遵守 Reactive Streams,自带背压契约,解决异步push的OOM风险;
同时再次呼应你上一个问题:
Mono 不是为背压而生,Mono/Flux 是Reactor为了区分「单结果异步任务」和「持续流式异步任务」设计的类型。
六、补充两个高频混淆点
-
异步 ≠ 非阻塞
异步是编程模型;非阻塞是IO模型。
你之前写的
Mono.fromCallable + subscribeOn(boundedElastic)是:异步编程模型 + 阻塞IO(JDBC) ,只是把阻塞放到独立线程池隔离。WebFlux 的 WebClient 是:异步编程模型 + 非阻塞IO(Netty epoll),是原生最优形态。
-
同步也可以限流,异步才需要背压
同步调用天然是"一问一答",调用者等返回,自带拉取限速;
一旦解耦变成异步推送,生产者和消费者不在同一个调用栈,这个天然约束消失,才需要背压协议来重建限速。
极简时间线浓缩版(方便放进md)
- 痛点:BIO 一个请求一个线程,IO等待时线程空耗,并发上不去
- OS 提供 epoll/IOCP:允许线程不卡死在IO上
- 原生回调:难写,回调地狱,异常、编排、取消很难处理
- 各种异步库出现(Guava、Netty、RxJava1):封装回调,方便异步任务编排,提升并发能力
- 新痛点:异步push模型没有统一控速,上游疯狂发数据,下游处理慢,队列无限膨胀OOM;并且各个异步流式API互不兼容
- 推出 Reactive Streams 规范:统一接口 + 标准化背压契约
如果你愿意,我可以把这一段替换/追加到md文档里,调整章节顺序,逻辑变成:
-
- 为什么出现异步、异步库(本章)
-
- 异步带来的新问题:背压是什么,生产者消费者
-
- Reactive Streams 规范(接口、时序、规则)
-
- Mono / Flux 和规范的关系
-
- 误区汇总
需要我直接输出更新后的完整md文本吗?