← 返回题目列表

阻塞、非阻塞、同步和异步 I/O 有什么区别?

高频 中等 第 3 / 22 题 更新于 2026/07/25
阻塞 I/O非阻塞 I/O同步 I/O异步 I/O

简化版

阻塞/非阻塞关心“调用时数据没准备好,当前线程是否等待”;同步/异步关心“I/O 完成由调用方主动执行和确认,还是提交后由系统在完成时通知”。非阻塞不等于异步:Java SocketChannel 非阻塞读仍由应用线程主动调用,而 AsynchronousSocketChannel 才提供完成回调或 Future

详细版

可以把两组概念拆成两个维度:

  • 阻塞:调用暂时不能完成时,当前线程停在调用处等待。
  • 非阻塞:调用暂时不能完成时,立即返回当前结果,例如返回 0 字节,应用程序之后再试。
  • 同步 I/O:应用程序在 I/O 就绪后主动执行 read / write,并在调用返回后获得处理结果。
  • 异步 I/O:应用提交 I/O 请求后继续工作,完成时通过回调、Future 等机制获取结果。

常见 Java 对应关系:传统 InputStream.read() 通常是同步阻塞;非阻塞 SocketChannel + Selector 是同步非阻塞的事件驱动用法;AsynchronousSocketChannelAsynchronousFileChannel 是 Java NIO.2 的异步通道抽象。

完整版教学

一、先区分“等待就绪”和“复制数据”

以网络读取为例,一次 I/O 至少可以概念化为两段:

  1. 等待网络数据进入内核接收缓冲区;
  2. 将数据交给应用程序可访问的缓冲区。

同步非阻塞模式下,线程不在第一段的某个 Socket 上死等,可以用 Selector 等待多个连接;但得到就绪通知后,仍由应用线程调用 read() 获取数据。因此它不是“发起以后什么都不用管”。

二、四种说法如何理解

模式典型表现Java 中的常见例子
同步阻塞调用 read 后等到有数据或结束InputStream、阻塞 SocketChannel
同步非阻塞read 无数据时立即返回,应用稍后重试非阻塞 SocketChannel
同步非阻塞 + 多路复用先等待就绪集合,再主动读写Selector 事件循环
异步提交操作,完成时收到结果AsynchronousSocketChannel

Selector.select() 本身可以阻塞等待事件,这与被它监听的 Channel 处于非阻塞模式并不矛盾。一个线程在一个 Selector 上睡眠,比很多线程分别在很多 Socket 上等待更容易管理。

三、异步通道如何返回结果

ByteBuffer buffer = ByteBuffer.allocate(4096);

channel.read(buffer, buffer, new CompletionHandler<>() {
    @Override
    public void completed(Integer count, ByteBuffer attachment) {
        if (count == -1) return;
        attachment.flip();
        // 处理读取到的数据
    }

    @Override
    public void failed(Throwable error, ByteBuffer attachment) {
        // 记录异常并清理连接
    }
});

AsynchronousSocketChannel 支持 CompletionHandlerFuture。使用 Future.get() 后立即在原地等待,会把异步 API 又用成阻塞风格;它仍是异步接口,但调用方并没有获得并发编排上的好处。

异步操作完成前,不能把同一个 Buffer 提前清空、归还池中或交给其他请求复用,否则底层 I/O 与应用修改会并发访问同一块数据。

四、实际选型不能只看名词

  • 连接数少、逻辑简单:同步阻塞代码最直观。
  • 大量长连接且单次处理很短:Selector 事件循环可以很高效,但状态机和半包处理更复杂。
  • 已有框架封装:优先遵循框架的线程模型,不要在事件线程中做长时间阻塞业务。
  • Java 21+ 虚拟线程能让很多阻塞 I/O 代码在保留简单编程模型的同时支撑高并发,但它不会让 I/O 本身变快,也不能消除数据库连接、外部服务等下游容量限制。

五、用时间线判断四个概念

假设客户端在 t=0 调用读取,数据在 t=80 ms 到达内核,复制到应用 Buffer 需要 2 ms。同步阻塞调用可能从 t=0 一直返回到 t=82 ms;同步非阻塞调用会在数据未到时立即返回 0,应用稍后再试;Selector 可让线程在一个等待点观察多个连接,某个连接就绪后仍由应用调用 read()

t=0          t=80 ms       t=82 ms
提交/尝试读取 ──等待数据──→ 数据就绪 ──复制/交付──→ 应用得到结果

异步 API 的关键不是“绝不使用线程”,而是调用方提交操作后不必亲自轮询就绪,并在完成时接收结果。底层可能利用内核异步设施,也可能借助实现管理的线程,API 合同并不承诺所有平台采用同一种内核机制。

问题阻塞/非阻塞维度同步/异步维度
暂时做不了是否立即返回
谁负责等待并交付最终完成结果
Selector 的典型归类非阻塞同步
CompletionHandler 的典型归类调用方不原地等待异步

心法:先画出“等待数据就绪”和“完成数据交付”两阶段,再判断调用方在哪一阶段等待、谁拿到最终结果,名词就不会混。

六、超时、取消与背压不能省略

非阻塞或异步只改变等待方式,不代表请求可以无限提交。若服务每秒只能完成 500 次写入,却持续接收 800 次请求,每秒会积压 300 个;10 秒后就是约 3,000 个待处理任务,最终表现为内存增长和尾延迟恶化。

工程上要给连接数、待写队列、并发异步操作和超时设置上界。取消 Future 或关闭异步通道的具体结果要按 API 合同处理,回调仍要区分正常完成、失败、超时和关闭,并确保 Buffer 不会被并发复用。

七、常见误区与追问

  • 误区:非阻塞 I/O 就是异步 I/O。 非阻塞 SocketChannel.read() 仍由应用主动调用并取得结果,通常属于同步非阻塞模型。
  • 误区:Selector.select() 会阻塞,所以整个模型就是阻塞 I/O。 Selector 可以阻塞在统一等待点,而被管理的通道仍是非阻塞的,两者并不矛盾。
  • 误区:使用 Future 就一定没有阻塞。 如果提交后立即调用 get() 原地等待,调用方仍会阻塞,只是使用了异步接口。
  • 误区:异步 I/O 不需要任何线程。 API 只规定完成通知语义,底层实现仍可能使用系统线程或线程池。
  • 追问:同步非阻塞为什么常和多路复用一起出现? 单纯轮询大量 Socket 会浪费 CPU,Selector 能集中等待就绪集合,避免无意义重试。
  • 追问:回调里能否立刻复用原 Buffer? 只有当前操作已经完成且没有其他并发操作持有它时才可以,否则会产生数据竞争或内容污染。
  • 追问:虚拟线程属于异步 I/O 吗? 对业务代码通常仍是同步阻塞风格,只是 JVM 能在许多阻塞点卸载虚拟线程,降低平台线程占用。

八、加强记忆

把两组概念分开记:阻塞与非阻塞看“当前调用要不要原地等”,同步与异步看“谁在完成 I/O 后交付结果”。非阻塞只代表这次调用不死等,不代表 I/O 完成后一定主动回调。