1. 操作系统 & 网络 IO 模型
IO的本质
IO = 数据在用户态 ↔ 内核态之间的拷贝,普通程序运行在用户态,而硬件资源由内核态统一管理,应用程序不能直接访问硬件,必须通过内核。
IO的两个阶段:
等待阶段(Waiting):数据是否从网卡到达内核缓冲区
拷贝阶段(Copying):内核缓冲区 → 用户缓冲区
在等待阶段可能发生阻塞/非阻塞,而在拷贝阶段可能发生同步/异步
阻塞/非阻塞:
阻塞 IO(Blocking IO)
阻塞 IO 在等待数据期间会让线程挂起
特点:调用 read() 后:若数据没准备好则线程阻塞,若数据准备好则继续执行。
编程简单,但线程资源浪费
非阻塞 IO(Non-blocking IO)
非阻塞 IO 解决了线程阻塞问题,但带来了 CPU 浪费
特点:调用read() 立即返回,若数据没好则返回 -1 / 异常,程序需要不断轮询
会导致CPU 空转
同步/异步:
同步 IO(Synchronous IO):
数据拷贝由应用线程发起并等待完成,即使是非阻塞 IO,也属于同步 IO
异步 IO(Asynchronous IO):
应用只发起请求,内核完成数据拷贝,通过回调 / 信号通知应用,应用无需等待
五种IO模型
阻塞IO(BIO)
工作流程:
read()调用→等待数据→拷贝数据→返回结果
特点:简单,并发能力差
非阻塞IO
工作流程:
不断调用read()→直到有返回数据
特点:CPU空转严重,实际很少使用
IO多路复用
一个线程监控多个 IO 事件,当某个 IO 就绪后再处理,是NIO的基础
Java NIO 实际为同步非阻塞IO+IO多路复用
Netty使用的是基于 epoll 的 IO 多路复用 + Reactor 模式
实际也是轮询,但是由内核实现轮询,CPU开销较低,并发能力强。
有三种实现:select/poll/epoll
信号驱动IO
数据准备好后发送信号,使用较少
异步IO(AIO)
内核完成所有操作,通过回调通知应用,Java AIO 属于这一类
2. Java BIO
BIO 是什么
Java BIO 是基于阻塞 IO 模型的同步 IO 实现
阻塞:线程在 read() / write() 时会挂起
同步:数据拷贝由调用线程完成
一连接一线程:典型并发模型
适用场景:低并发、内部工具、管理后台、简单文件处理
不适用于高并发网络服务与长连接场景
BIO 的整体工作模型
现在有一个 Java 服务端,一个客户端连进来发数据
服务端启动:
ServerSocket serverSocket = new ServerSocket(8080);此时程序启动,只有一个主线程,没有IO发生
accept():
Socket socket = serverSocket.accept();这里是第一个阻塞点,如果还没有客户端连接,主线程就在这里阻塞,一旦有客户端连进来,accept() 就会返回一个 Socket
拿到 Socket 后准备管道:
InputStream in = socket.getInputStream();read():
in.read(buffer);这里是第二个阻塞点,如果客户端还没发数据,则当前线程阻塞等待,若客户端发数据了,则内核拷贝数据,read()返回
那如果有多个客户端连接怎么办?
BIO 是一连接一线程机制:
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待连接
new Thread(() -> {
try {
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
in.read(buf); // 阻塞等待数据
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}总结:Java BIO 采用的是同步阻塞模型。服务端通常使用 ServerSocket 在主线程中阻塞 accept 连接,每当有一个客户端连接,就创建一个线程专门处理该连接,线程在 read/write 时会一直阻塞等待数据。因此 BIO 是典型的一连接一线程模型,并发能力受限。
BIO 的核心问题
线程资源浪费:
大量线程处于阻塞状态
线程栈内存占用大(1MB 左右 / 线程)
上下文切换成本高:
线程越多 → 调度越频繁
C10K 问题:
1 万连接 ≈ 1 万线程 → 系统崩溃
Java IO 流体系结构
字节流:
抽象类:
InputStream
OutputStream
常见实现:
FileInputStream / FileOutputStream
BufferedInputStream / BufferedOutputStream
ObjectInputStream / ObjectOutputStream
ByteArrayInputStream / ByteArrayOutputStream
使用场景:
图片、视频、压缩包、二进制数据
字符流:
抽象类:
Reader
Writer
常见实现:
FileReader / FileWriter
BufferedReader / BufferedWriter
InputStreamReader / OutputStreamWriter
使用场景:
文本数据(涉及编码)
对比:
节点流、处理流
节点流:
直接连接数据源,FileInputStream、FileReader
处理流:
包装节点流,增强功能
常见:
缓冲(BufferedXXX):内部维护一个 byte[] / char[],应用与系统之间多一层缓冲,通过空间换时间,提高 IO 性能,减少系统调用,一次读取一块数据。
转换(InputStreamReader)
序列化(ObjectXXX)
例如:
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("test.txt")
)
);字符编码 & InputStreamReader
当字节 → 字符 的编码不一致时,会出现乱码。
InputStreamReader 可以在字节流转换为字符流时指定编码:
new InputStreamReader(
new FileInputStream("a.txt"),
StandardCharsets.UTF_8
);3. Java NIO
NIO 三大组件
IO 多路复用
一个 Selector 监控多个 Channel ,哪个 Channel 就绪了处理哪个
工作流程
程序启动:
Selector selector = Selector.open();创建 ServerSocketChannel:
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);Channel 可以设置成非阻塞
把 server 注册到 selector:
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);集中管理事件,当有连接发生时通知Selector,此时 selector 里记录了一个Channel和一个关心的事件类型。
select():
selector.select(); // 阻塞等待事件发生,注意Selector 阻塞的是“事件”而不是某个具体的连接
当事件发生时(一个客户端连接/已连接的客户端发送数据),OS 通知 Selector,select()返回
处理就绪事件:
Set<SelectionKey> keys = selector.selectedKeys();select()返回后拿到数据:哪个 Channel 发生了什么事。遍历 OP_ACCEPT 接收数据,遍历 OP_READ 读取数据
读数据:
SocketChannel channel = (SocketChannel) key.channel();
channel.read(buffer); // 没数据直接返回其他
select() 存在空查询bug:select() 可能空轮询,导致 CPU 100%,原因为JDK epoll Bug,select() 返回 0实际无事件(Netty 通过重建 Selector 解决)
4. 零拷贝
普通IO的拷贝过程
普通IO一共要经过四次拷贝:
磁盘 → 内核缓冲区:DMA 把数据从磁盘读到 内核 buffer
内核缓冲区 → JVM 堆(byte[]):read() 把数据拷到 byte[]
JVM 堆 → Socket 内核缓冲区:write() 再拷一遍
Socket 缓冲区 → 网卡:DMA 发送
零拷贝的思想
在普通IO的四次拷贝中,从内核缓冲区到JVM、从JVM堆到Socket内存缓冲区的两次拷贝是多余的,零拷贝省去这两次拷贝,数据不再经过用户态,直接在内核态完成“文件 → 网络”的转发。
适用于大文件传输,日志同步,MQ消息转发等。
为什么零拷贝能提升性能?
减少了CPU 拷贝次数,无需用户态 ↔ 内核态切换,减少了 CPU Cache 污染
零拷贝的使用
普通IO的使用方式:
FileInputStream fis = new FileInputStream("a.txt");
SocketOutputStream out = socket.getOutputStream();
byte[] buf = new byte[8192];
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}零拷贝使用FileChannel.transferTo / transferFrom:
FileChannel fileChannel = new FileInputStream("a.txt").getChannel();
SocketChannel socketChannel = SocketChannel.open();
fileChannel.transferTo(0, fileChannel.size(), socketChannel);使用 transferTo后,拷贝过程变为:磁盘 → 内核缓冲区 → Socket 缓冲区 → 网卡
没有 JVM ↔ 内核来回拷贝
还有一种DirectByteBuffer(堆外内存)的使用:
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);堆外内存不是完全零拷贝,但也减少了拷贝次数。其内存不在 JVM 堆,可以被内核直接访问,少一次:JVM堆 → 内核 的拷贝
底层原理
零拷贝靠的是这两种机制之一:
sendfile(真正的零拷贝):由Linux 提供,内核直接把文件发到 socket,CPU 几乎不参与
mmap(内存映射):文件映射到内存,可以减少一次拷贝
具体使用哪种,由 Java 根据平台选择决定
5. Reactor 模式
Reactor 模式是什么
Reactor 模式是一种基于事件驱动的并发处理模型,它通过一个或多个 Reactor 线程监听 IO 事件,将就绪事件分发给对应的处理器,从而实现高并发网络通信。
解决了BIO一连接一线程,线程阻塞的问题,直接 NIO,IO 线程被业务逻辑拖慢的问题。将 IO 监听与业务处理解耦,用少量 IO 线程支撑大量并发连接。
核心组成
工作流程
Channel 注册到 Selector →
Reactor 线程调用 select() 等待事件 →
事件就绪后,获取 SelectionKey →
根据事件类型分发给 Handler →
Handler 进行处理或提交给线程池
Reactor 只负责“通知和分发”,不负责具体业务。
这种设计使得 Reactor 不会被某个较慢的具体业务拖慢性能。
三种 Reactor 模型
单 Reactor 单线程
一个 Reactor 线程同时负责 IO 事件监听、事件分发以及业务处理。
线程结构:
1 个 Reactor 线程
- select
- accept
- read
- write
- 业务逻辑实现简单,但当任一业务阻塞就会阻塞所有连接
单 Reactor 多线程
一个 Reactor 线程负责 IO 事件监听和分发,业务处理交由 Worker 线程池完成。
1 个 Reactor(IO)
N 个 Worker(业务)IO 线程不阻塞,但 accept 和 IO 仍可能成为瓶颈,适用于大多数 Web 应用
主从 Reactor 多线程
通过多个 Reactor 分工协作,主 Reactor 负责连接接入,从 Reactor 负责 IO 读写,业务逻辑由独立线程池处理。
线程结构:
Main Reactor(accept)
Sub Reactor(read / write)
Worker Pool(业务)并发能力最强,但实现复杂
Netty、Tomcat NIO、Kafka 使用了这种模式
Reactor 与 BIO 与 NIO
Java NIO 提供了 Channel、Selector 等底层工具,Reactor 模式则定义了如何使用这些工具来构建高并发服务器。Reactor 是一种设计模式,而 NIO 是具体的 API。
6. Java AIO
AIO 是什么
Java AIO 是基于异步 IO 模型的非阻塞 IO 实现,应用发起 IO 请求后立即返回,IO 操作由操作系统完成,并通过回调通知应用结果。
AIO 与 NIO 的本质区别
工作流程
应用发起异步 IO 请求 →
方法立即返回 →
内核完成 IO 操作 →
回调 CompletionHandler →
应用处理结果
优缺点
优点是真正异步,理论并发能力强,无需 select / 轮询
缺点是操作系统支持有限(很多实现是“线程 + 回调”的假异步,本质仍是阻塞 IO 封装),编程复杂度高且生态不成熟
生产环境很少使用AIO:Java AIO 在 Linux 平台上底层支持不完善,网络 IO 实现效果有限,实际生产中更常用 NIO + Reactor + Netty 的组合,兼顾性能、稳定性和生态成熟度。
核心 API
核心 Channel:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
CompletionHandler:
channel.read(buffer, attachment, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
// IO 完成后的回调
}
@Override
public void failed(Throwable exc, Object attachment) {
// IO 失败
}
});IO 完成后才回调,不阻塞任何线程
AIO vs Reactor
7. Netty
Netty 是什么
Netty 是一个基于 Java NIO 的高性能网络通信框架,通过封装 Reactor 模型、内存管理、线程模型和零拷贝机制,简化并优化了高并发网络编程。
原生NIO存在以下问题:
API 复杂:状态多、代码冗长
易出 Bug:Selector 空轮询、OP_WRITE 滥用
无协议支持:粘包/半包自己处理
内存管理差:ByteBuffer 不好用
线程模型要自研:Reactor 要自己实现
而 Netty 提供了解决方案,封装 Reactor 模型,高性能 ByteBuf,内存池,零拷贝,Pipeline 责任链,解决了 JDK NIO Bug。
Netty 的 IO 线程模型
boss / worker 线程模型(主从 Reactor 多线程模型):
BossGroup(Main Reactor)
- 接收连接(OP_ACCEPT)
WorkerGroup(Sub Reactor)
- 处理 IO(OP_READ / OP_WRITE)当发生一次请求时:
Boss 接收连接
→ 分配 SocketChannel 给 Worker
→ Worker 监听读写事件
→ 读到数据后,进入 Pipeline
→ 业务 Handler 处理
→ 写回响应
优点:accept 不阻塞 IO,IO 不阻塞业务,线程职责清晰,易扩展
Netty 的 Channel & EventLoop
EventLoop = 一个线程 + 一个 Selector + 一个任务队列
一个 Channel 始终绑定同一个 EventLoop,可以避免并发问题,也无需加锁
而EventLoopGroup管理多个 EventLoop,提供线程池能力
ByteBuf vs ByteBuffer
ByteBuffer 的问题:
ByteBuf 的优势:
零拷贝
Netty 的零拷贝是“多种技术组合”,不是一种实现方式。
常见方式有:
FileRegion(sendfile):
文件 → socket
基于 FileChannel.transferTo
CompositeByteBuf:
多个 ByteBuf 逻辑合并
不发生真实拷贝
slice / duplicate:
共享底层内存
只改索引
Pipeline & Handler
Pipeline 是一个双向责任链,用于处理入站和出站事件。
使用 Pipeline 可以解耦协议处理,解耦业务逻辑,实现可插拔
而常见 Handler 有解码器、编码器、业务处理器