异步和阻塞 IO 的区别是什么?
更新时间:2026-09-18。问题驱动短答页。事件循环被阻塞的排查见 async/await 与事件循环阻塞,流控见 Reactor Flux 背压,线程模型见 Netty Reactor。
一句话回答
区别在于「等数据的这段时间,线程在干什么」:阻塞 IO 时线程发起 read 就挂起,直到内核数据就绪被唤醒,等待期间这个线程什么也做不了、只能干等;非阻塞 + IO 多路复用(epoll/IOCP)时,线程不傻等,而是通过事件循环同时照看大量连接,哪个连接就绪了再去处理哪个——因此少量线程就能撑住海量并发连接,而不必「一个连接一个线程」。
直观对比
| 维度 | 阻塞 IO(thread-per-connection) | 非阻塞异步(事件驱动) |
|---|---|---|
| 等待时线程 | 挂起,不占 CPU 但占着线程/栈内存 | 不等待,继续处理其它就绪事件 |
| 并发模型 | 一连接一线程,连接多则线程多 | 少量事件循环线程管海量连接 |
| 内存 | 每线程 ~MB 栈,万连接 ~GB | 连接只是对象,成本低 |
| 切换 | 线程多、切换频繁 | 线程少,切换少 |
| 编程复杂度 | 直观、顺序代码 | 回调/Future/async-await,需理解流控 |
一个关键澄清:异步不是「更快」,而是「更能扛并发」
- 对单个已经就绪的请求,异步不会让计算变快;它省的是「等待」期间的线程占用。
- 价值在高并发、IO 密集(大量连接、大量等待下游/磁盘):用固定少量线程摊薄内存与上下文切换成本。
- CPU 密集计算用异步没收益,反而增加复杂度。
最大的坑:异步链路里写阻塞代码
事件循环线程很少(常每核一个)。只要在回调里调用了阻塞操作——同步 JDBC、block()、同步 HTTP 客户端、Thread.sleep、锁竞争——这个线程就被卡死,它负责的所有连接全部停摆,吞吐断崖。这正是 WebFlux/Reactor 背压文反复强调「全链路非阻塞」的原因;实在要做阻塞活,丢到专门的弹性线程池(boundedElastic)。
常见疑问
- 非阻塞等于异步吗? 非阻塞是「调用立即返回」,异步是「结果稍后通过回调/Future 通知」;常配合使用但概念不同。
- 为什么还需要线程池? 阻塞模型靠线程池复用线程避免反复创建;异步模型用少量事件线程 + 少量 worker 处理阻塞任务。
- 背压和这有什么关系? 事件驱动下快生产者会瞬间灌爆下游,需要需求信号反压,见 Reactor 背压。
总结
阻塞 IO 在等数据期间把线程挂起干等,高并发只能一连接一个线程,万级连接就要万级线程、吃掉 GB 级栈内存并带来频繁上下文切换;非阻塞加 IO 多路复用让少量事件循环线程通过 epoll 同时照看海量连接、就绪才处理,用很低的线程与内存成本扛住高并发,所以异步的价值是「更能扛 IO 并发」而非让单个请求计算更快;成败关键是事件线程里绝不能出现阻塞调用,否则少量事件线程一卡死就拖垮它负责的全部连接、退化成比阻塞模型更糟的系统,阻塞活应放到弹性线程池并保证全链路非阻塞与背压贯通。