同步、异步、阻塞、非阻塞怎么理解?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
概念区分度:面试官不仅仅是想知道你能不能背出四个定义,更是想知道你是否意识到这是两组正交的概念:一组描述消息通知机制,一组描述等待时的线程状态。
-
IO 模型的映射能力:能不能把这四个概念落到具体的 IO 模型上(BIO、NIO、AIO、多路复用),而不是停留在抽象名词层面。
-
技术视野:是否了解 Linux 下 epoll、io_uring、Windows IOCP 这些真实实现,知道 “异步” 在操作系统层面到底是怎么兑现的。
核心答案
先甩结论:同步/异步说的是 “结果怎么拿”,阻塞/非阻塞说的是 “等结果的时候你能不能走”。这是两个不同维度,不是反义词的两端。
| 维度 | 关注点 | 一句话区分 |
|---|---|---|
| 同步 vs 异步 | 被调用方如何通知结果 | 同步:结果出来前,调用方得自己盯着;异步:被调用方办完了主动喊你 |
| 阻塞 vs 非阻塞 | 调用方等待时的状态 | 阻塞:调用发起后线程挂起干等;非阻塞:调用立即返回,线程该干嘛干嘛 |
两者组合,就得到了四种经典 IO 形态:
| 组合 | 名称 | 典型代表 |
|---|---|---|
| 同步阻塞 | BIO | Java Socket、老式 read() |
| 同步非阻塞 | NIO + 轮询 | recv(MSG_DONTWAIT)、Java NIO |
| 同步非阻塞 + 多路复用 | IO 多路复用 | select / poll / epoll |
| 异步非阻塞 | AIO | Windows IOCP、Linux io_uring |
注意,没有 “异步阻塞” 这个组合——都异步通知了还阻塞等着,逻辑上矛盾,属于人为造出来的伪命题。
深度解析
一、先上烧水的比喻,绕不过去的经典
小明想烧壶开水。这四个概念用烧水场景翻译一遍:
- 同步阻塞:小明把水壶放灶上,然后站在灶边盯着,水不开哪儿也不去。人(线程)被这件事完全占住了。
- 同步非阻塞:小明把水壶放灶上,转身去看电视了,每隔几分钟跑回厨房瞅一眼 “开了没”。人没被占住,但 “水开了” 这个结果还得小明自己来确认。
- 同步非阻塞 + 多路复用:小明烧的不止一壶水,厨房里十几个灶眼。他雇了个管家(
epoll),谁家水开了管家就喊一声,小明再过去处理。一个 “人” 搞定了 N 个灶。 - 异步非阻塞:小明用的是会响的水壶,水开了 “嘀嘀” 自动叫,而且壶自己把水灌进暖瓶,小明只需要处理 “灌好的开水”。连 “确认水开” 和 “灌水” 这步都省了。
看出差别了吗?同步和异步的差别在于 “谁去确认结果”,阻塞和非阻塞的差别在于 “等的时候人在不在场”。
二、回到操作系统层面
抽象比喻落到代码上,以 read() 读网卡数据为例。数据从网卡到你的程序,要经历两个阶段:
上图的两个阶段,是区分四种模型的关键:
- 阶段一:数据从网卡到达内核缓冲区(等待数据就绪)
- 阶段二:数据从内核缓冲区拷贝到用户空间(真正的 IO 操作)
下面把四种模型对着这两个阶段过一遍:
- 同步阻塞(BIO):阶段一,线程调用
read()后挂起,内核等数据、拷数据,全程线程睡大觉,直到两个阶段都完成才被唤醒。简单直接,但一个线程只能伺候一个连接。 - 同步非阻塞(NIO):
read()立刻返回。数据没就绪就返回个-1(EWOULDBLOCK),线程不被挂起,可以过会儿再来问。代价是得不停地轮询,CPU 在空转。 - IO 多路复用:还是同步非阻塞的底子,但加了个
epoll这样的中间人。线程把一批 fd 交给内核:“这几个连接谁就绪了告诉我”。内核就绪了才通知线程,线程再对就绪的连接调read()。阶段二的数据拷贝,仍然是线程自己触发的,所以说它本质还是同步。 - 异步 IO(AIO):线程发起
aio_read后彻底不管了。内核把阶段一、阶段二全干完,数据已经躺在用户缓冲区了,才通知线程 “直接用吧”。这才是 POSIX 定义下的异步。
这里有个高频误区要掰扯清楚:为什么 epoll 是同步的?
很多人觉得 epoll 事件通知了,就是异步。不对。epoll 只帮你完成了 “监听就绪”(阶段一的通知),阶段二 “内核缓冲区 → 用户缓冲区” 的拷贝,还得你自己在收到通知后调用 read() 去完成。只要数据拷贝这一步是调用方自己动手的,就是同步。异步的要求更苛刻:连拷贝都不用你管。
用一张时序图对比一下 NIO 多路复用和 AIO 的差别:
上图的分水岭就一条:read 或者说数据拷贝这个动作,是线程自己触发的(同步),还是内核包圆了只给你个结果(异步)。
三、Java 里的对应关系
Java 程序员对这四个词的体感,基本来自 JDK 的 IO 演进:
- BIO(
java.io):同步阻塞。ServerSocket.accept()一堵,一个连接一个线程,连接多了线程先爆。 - NIO(
java.nio,JDK 1.4):同步非阻塞。Channel+Buffer+Selector,一个线程管一堆连接,Netty 就是在这套东西上长出来的。 - AIO(
java.nio.channels.AsynchronousChannel,JDK 7):异步非阻塞。回调CompletionHandler风格。Windows 底层走 IOCP(真异步),Linux 上则用 epoll 模拟,所以 Java 的 AIO 在 Linux 上并没有真正的性能优势,这也是 Netty 后来干脆移除 AIO 传输实现的原因之一。
顺带说一句 Linux 原生那边:老的 Linux AIO(io_setup 那一套)限制很多,比如对缓冲区对齐的要求,实际没铺开;2019 年内核 5.1 引入的 io_uring 才算把真异步 IO 这事做扎实了,现在成了高性能网络库的新宠。
四、常见误区
- 误区一:同步 = 阻塞。不等号。同步非阻塞就是活生生的反例:轮询确认结果是同步的,但轮询间隙线程没挂起,是非阻塞的。
- 误区二:多路复用是异步。前面说了,数据拷贝还得自己干,属于同步阵营。Unix 网络编程里把阻塞、非阻塞、多路复用、信号驱动这四种全划为同步 IO,异步只留 AIO 一个独苗。
- 误区三:异步阻塞存在。面试偶尔有人被带节奏说出了这个词。异步的通知机制决定了发起方根本不用等,配阻塞属于逻辑矛盾,别被绕进去。
面试高频追问
-
追问一:
select、poll、epoll的区别?select有 1024 个 fd 上限且每次调用都要全量拷贝、线性扫描;poll去掉了数量上限但扫描方式没变;epoll用红黑树管理 fd、就绪链表回调,只返回就绪的,时间复杂度 O(1) 级别,百万连接下优势碾压。
-
追问二:为什么 Redis、Nginx 都用多路复用而不是 AIO?
- 生态和平台原因。Linux 上真异步 AIO 长期不好用,
io_uring太新;而epoll成熟稳定、性能已经足够好,配合单线程事件循环模型刚好契合。
- 生态和平台原因。Linux 上真异步 AIO 长期不好用,
-
追问三:Java NIO 的
Selector底层是什么?- 依赖操作系统多路复用器,Linux 2.6+ 默认是
epoll(通过epoll_create/epoll_ctl/epoll_wait)。
- 依赖操作系统多路复用器,Linux 2.6+ 默认是
常见面试变体
- 变体一:“BIO、NIO、AIO 的区别?”——同一个考点,换皮问法,直接拿本文的表格答。
- 变体二:“为什么说
epoll是同步的?”——单独把误区二拎出来考。 - 变体三:“讲讲 Unix 的五种 IO 模型”——多了个信号驱动 IO(
SIGIO),实际很少用,提一嘴即可。
记忆口诀
同步看拷贝,阻塞看挂起:数据从内核到用户这步谁动手,定同步异步;调用发出去线程睡不睡,定阻塞非阻塞。
总结
一句话:同步/异步说的是结果获取方式,阻塞/非阻塞说的是等待时的线程状态,两组概念正交组合出四种 IO 形态。记住 “epoll 也是同步的,AIO 才是异步” 这条分界线,再把烧水的例子讲给面试官听,这道题基本就稳了。
