Q1:I/O是什么?
A1:
核心概念:什么是 I/O?🔄
定义
I/O = Input/Output(输入/输出) ,指数据在计算机内部(CPU/内存)与外部设备之间流动的过程。
数据流动方向
输入 (Input):外部 → 内存
• 键盘输入 → 程序
• 磁盘文件 → JVM 堆
• 网络请求 → Socket 缓冲区
• 数据库查询结果 → 应用内存
输出 (Output):内存 → 外部
• 程序日志 → 磁盘文件
• HTTP 响应 → 网络客户端
• 计算结果 → 数据库
• 监控指标 → Prometheus
I/O 的本质:速度不匹配的桥梁
图片来自https://www.mianshiya.com/

I/O 的分类
按设备类型分类
| 类型 | 示例 | 特点 | Java 对应 |
|---|---|---|---|
| 磁盘 I/O | 读写文件、数据库 | 慢(毫秒级)、顺序/随机访问 | FileInputStream, RandomAccessFile |
| 网络 I/O | HTTP/RPC/Socket | 慢(网络延迟)、流式传输 | Socket, HttpClient, Netty |
| 终端 I/O | 键盘、屏幕、日志 | 人机交互、缓冲输出 | System.in/out, Logger |
| 设备 I/O | GPU、网卡、USB | 专用协议、驱动交互 | JNI, JNA |
Q2:为什么网络I/O会被阻塞?
A2:
核心原因 1:内核缓冲区没数据(Read 阻塞)
这是最常见的阻塞场景。当你调用 socket.read() 时,数据并不是直接从网卡到你的 Java 程序,中间经过了内核缓冲区。
数据流动过程
网络线路 → 网卡 → 内核接收缓冲区 (Kernel Receive Buffer) → 用户缓冲区 (Java Heap)
阻塞发生点
- Java 应用 调用
read()。 - 操作系统 检查 内核接收缓冲区。
- 判断 :
- 有数据:拷贝到用户缓冲区,返回数据长度。
- 无数据 :阻塞当前线程,将线程放入等待队列,直到网卡收到新数据并唤醒线程。
形象类比
取快递: 你去快递柜(内核缓冲区)取包裹(数据)。
• 如果柜子里有货 → 直接拿走(返回)。
• 如果柜子是空的 → 你只能在旁边干等(阻塞),直到快递员把货放进柜子(网卡收到数据)。
Java 代码体现
java
Socket socket = new Socket("server", 8080);
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
// 如果服务器不发数据,这里永远卡住!
int len = in.read(buf); // 线程进入 BLOCKED/WAITING 状态(OS 层面)
核心原因 2:内核缓冲区满了(Write 阻塞)
写操作也会阻塞!很多人误以为 write() 只是把数据发到内存,应该很快。但如果网络慢,内存也会满。
阻塞发生点
- Java 应用 调用
write()。 - 操作系统 尝试将数据拷贝到 内核发送缓冲区 (Kernel Send Buffer)。
- 判断 :
- 有空间:拷贝成功,立即返回(数据还在内核,没真正发出去)。
- 空间满 :阻塞当前线程,等待 TCP 协议栈把缓冲区的数据发走,腾出空间。
为什么会满?
- 网络带宽不足:发送速度 > 网卡发送速度。
- 对端接收慢 :对端的接收窗口(Receive Window)满了,触发 TCP 流控,本端停止发送。
- 网络拥塞 :TCP 拥塞控制 限制了发送速率。
Java 代码体现
java
OutputStream out = socket.getOutputStream();
byte[] data = new byte[10 * 1024 * 1024]; // 10MB 大数据
// 如果网络极慢或对端不读,这里会卡住!
out.write(data); // 等待内核发送缓冲区腾出空间
核心原因 3:TCP 协议状态等待(Connect/Accept 阻塞)
在连接建立阶段,阻塞是由 TCP 状态机决定的。
1. Connect 阻塞(三次握手)
java
// 客户端
Socket socket = new Socket();
// 阻塞直到三次握手完成,或超时
socket.connect(new InetSocketAddress("server", 8080));
- 原因 :必须收到服务端的
SYN+ACK才能建立连接。如果服务端挂了、防火墙丢了包、或网络延迟高,客户端会一直等(直到 TCP 超时,通常几十秒)。
2. Accept 阻塞(服务端等待连接)
java
// 服务端
ServerSocket server = new ServerSocket(8080);
// 阻塞直到有新连接请求到达
Socket client = server.accept();
- 原因:内核监听队列(Backlog)中没有已完成的连接,线程必须等待。
Q3:I/O模型都有哪些?
A3:
5 种经典 I/O 模型(POSIX 定义)
1️⃣ 阻塞 I/O(Blocking I/O - BIO)
机制:两个阶段都阻塞。
- 应用发起
recvfrom。 - 内核等待数据准备(阻塞)。
- 数据准备好后,内核拷贝到用户空间(阻塞)。
- 返回结果。
java
应用线程:[ 阻塞等待数据 ] → [ 阻塞拷贝数据 ] → 继续执行
内核: [ 准备数据... ] → [ 拷贝数据... ]
- 优点:编程简单,逻辑清晰。
- 缺点:一个线程只能处理一个连接,并发低。
- Java 对应 :
java.io.Socket,ServerSocket。
2️⃣ 非阻塞 I/O(Non-blocking I/O - NIO*)
机制:两个阶段都不阻塞(轮询)。
- 应用发起
recvfrom。 - 内核立即返回(如果数据没准备好,返回
EWOULDBLOCK错误)。 - 应用不断轮询(while 循环),直到数据准备好。
- 数据准备好后,再次发起拷贝。
java
应用线程:[ 问:好了吗?❌ ] → [ 问:好了吗?❌ ] → [ 问:好了吗?✅ ] → [ 拷贝数据 ] → 继续
内核: [ 准备数据... ] → [ 准备数据... ] → [ 数据就绪 ] → [ 拷贝数据 ]
- 优点:线程不阻塞,可以做别的事。
- 缺点:轮询浪费 CPU 资源(忙等)。
- Java 对应 :
SocketChannel.configureBlocking(false)(单纯非阻塞模式,少用)。
注意 :Java 的 "NIO" 包其实主要用的是 模型 3(多路复用),而不是这个纯轮询的非阻塞 I/O。
3️⃣ I/O 多路复用(I/O Multiplexing)⭐ 最常用
机制:阶段 1 阻塞(监听),阶段 2 阻塞(拷贝)。
- 应用发起
select/poll/epoll,监听多个 FD(文件描述符)。 - 内核阻塞等待,直到任何一个FD 数据准备好。
- 返回就绪的 FD 列表。
- 应用对就绪的 FD 发起
recvfrom拷贝数据。
java
应用线程:[ 阻塞监听多个 FD ] → [ 收到就绪通知 ] → [ 拷贝数据 ] → 继续
内核: [ 监控多个连接... ] → [ 某个数据就绪 ] → [ 拷贝数据 ]
- 优点:一个线程可以处理成千上万个连接,CPU 利用率高。
- 缺点:编程复杂(需要处理就绪事件);数据拷贝阶段仍阻塞。
- Java 对应 :
java.nio.Selector,Netty,Redis,Nginx。
4️⃣ 信号驱动 I/O(Signal Driven I/O)
机制:阶段 1 非阻塞(信号通知),阶段 2 阻塞(拷贝)。
- 应用开启信号驱动,发起
recvfrom后立即返回。 - 内核准备数据,准备好后发送
SIGIO信号给应用。 - 应用收到信号,发起
recvfrom拷贝数据
java
应用线程:[ 注册信号 ] → [ 做别的事... ] → [ 收到信号✅ ] → [ 拷贝数据 ] → 继续
内核: [ 准备数据... ] → [ 发送信号 ] → [ 拷贝数据 ]
- 优点:等待数据时不阻塞。
- 缺点:信号处理复杂,容易出错,Linux 支持不完善。
- Java 对应:基本无直接支持。
5️⃣ 异步 I/O(Asynchronous I/O - AIO)⭐ 最理想
机制:两个阶段都不阻塞。
- 应用发起
aio_read,提供缓冲区指针和回调函数。 - 内核等待数据准备 并 拷贝到用户缓冲区。
- 全部完成后,通知应用(回调/信号)。
java
应用线程:[ 发起请求 ] → [ 做别的事... ] → [ 收到完成通知✅ ] → 继续
内核: [ 准备数据... ] → [ 拷贝数据... ] → [ 发送通知 ]
- 优点:真正的异步,应用完全不参与 I/O 过程。
- 缺点:实现复杂,Linux 底层 AIO 支持不完善(性能有时不如 epoll)。
- Java 对应 :
java.nio.channels.AsynchronousSocketChannel(使用较少)。