Java I/O 模型通俗学习(含完整示例):从 BIO 到 NIO/AIO + 零拷贝
Java I/O 模型通俗学习(含完整示例):从 BIO 到 NIO/AIO + 零拷贝
本文会用 “人话” 拆解 Java 中的 3 大 I/O 模型(BIO、NIO、AIO)和零拷贝技术,避开复杂术语,结合实际场景讲清核心逻辑、底层依赖和使用场景,适合新手快速入门。
一、先搞懂:I/O 到底在做什么?
I/O(输入/输出)本质是“数据在设备(磁盘、网卡)和内存之间的移动”,比如:
- 读文件:磁盘里的文件 → 内存(程序能访问);
- 发网络数据:内存里的程序数据 → 网卡 → 另一台机器;
Java 程序要做 I/O,不能直接操作硬件(磁盘、网卡),必须“拜托操作系统内核帮忙”——这就像你(程序)想拿快递(数据),不能直接去快递站(硬件),得让快递员(内核)帮你取,你只需要等快递员把快递(数据)送上门(内存)。
二、传统 I/O(BIO):简单但低效的“傻等”模式
BIO 是 Java 最早期的 I/O 方式,核心是“同步阻塞”,通俗说就是“发起请求后,啥也不干,一直等结果”。
1. 核心场景示例:BIO 实现“读文件+发网络”
服务端(接收文件)
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO 服务端:同步阻塞,一个连接一个线程
*/
public class BioFileServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("BIO 服务端启动,等待客户端连接...");
// 循环监听连接(阻塞在 accept())
while (true) {
Socket clientSocket = serverSocket.accept(); // 阻塞:直到有客户端连接
System.out.println("客户端连接成功:" + clientSocket.getInetAddress());
// 为每个连接创建新线程(高并发时线程爆炸)
new Thread(() -> {
try (InputStream in = clientSocket.getInputStream();
FileOutputStream fos = new FileOutputStream("bio_received.txt")) {
byte[] buffer = new byte[1024]; // 用户态缓冲区(堆内存)
int len;
// 阻塞在 read():直到有数据可读
while ((len = in.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.out.println("文件接收完成(线程:" + Thread.currentThread().getName() + ")");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}客户端(读取文件并发送)
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* BIO 客户端:同步阻塞,读文件+发网络
*/
public class BioFileClient {
public static void main(String[] args) throws Exception {
// 连接服务端(阻塞:直到连接建立)
Socket socket = new Socket("127.0.0.1", 8080);
// 读本地文件(阻塞在 read())
try (FileInputStream fis = new FileInputStream("test.txt");
OutputStream out = socket.getOutputStream()) {
byte[] buffer = new byte[1024]; // 用户态缓冲区
int len;
// 阻塞:直到文件读取完成
while ((len = fis.read(buffer)) != -1) {
out.write(buffer, 0, len); // 阻塞:直到数据发送完成
}
System.out.println("BIO 客户端文件发送完成");
}
socket.close();
}
}2. 核心问题:“无用功”太多
- 两次 CPU 拷贝:文件从内核→用户→内核,CPU 全程在搬数据,没做业务逻辑;
- 线程浪费:一个连接一个线程,1 万个连接就要 1 万个线程,服务器扛不住;
- 阻塞严重:
accept()、read()、write()都会阻塞,线程利用率极低。
3. 适用场景
低并发、小文件传输(比如简单的 TCP 聊天程序),代码简单好理解,但高并发场景直接“趴窝”。
三、NIO:高效的“多任务管家”模式
NIO(New I/O)是对 BIO 的升级,核心是“同步非阻塞 + 事件驱动”,通俗说就是“请一个管家(Selector)帮你盯着所有快递,你只处理已经到的快递,不用傻等”。
1. 核心场景示例:NIO 实现“高并发文件传输”
服务端(单线程处理多连接)
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.Set;
/**
* NIO 服务端:单线程处理多连接,事件驱动
*/
public class NioFileServer {
public static void main(String[] args) throws Exception {
// 1. 创建服务端通道(非阻塞)
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(8080));
// 2. 创建 Selector(管家)
Selector selector = Selector.open();
// 3. 注册“连接事件”到 Selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务端启动,监听 8080 端口...");
while (true) {
// 4. 阻塞等待事件就绪(可设置超时)
selector.select();
// 5. 处理就绪事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 避免重复处理
// 处理“连接事件”
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = channel.accept(); // 非阻塞:立即返回
clientChannel.configureBlocking(false);
System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
// 注册“读事件”+ 绑定缓冲区(堆外内存)
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
}
// 处理“读事件”(客户端发送数据)
else if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment(); // 复用缓冲区
int len = clientChannel.read(buffer); // 非阻塞:无数据返回 -1
if (len > 0) {
buffer.flip(); // 切换为读模式
// 写入本地文件
try (FileOutputStream fos = new FileOutputStream("nio_received.txt", false)) {
fos.getChannel().write(buffer);
}
buffer.compact();
} else if (len == -1) {
System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
clientChannel.close(); // 客户端断开连接
}
}
}
}
}
}客户端(非阻塞+零拷贝传输)
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.util.Iterator;
import java.util.Set;
/**
* NIO 客户端:非阻塞 + 零拷贝(transferTo)
*/
public class NioFileClient {
public static void main(String[] args) throws Exception {
// 1. 创建 Socket 通道(非阻塞)
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 2. 连接服务端(非阻塞:立即返回)
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
// 3. 创建 Selector,监听“连接完成事件”
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 4. 打开文件通道
FileChannel fileChannel = new FileInputStream("test.txt").getChannel();
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 处理“连接完成”
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.finishConnect()) {
System.out.println("NIO 客户端连接成功");
// 切换监听“写事件”(Socket 缓冲区可写)
channel.register(selector, SelectionKey.OP_WRITE);
}
}
// 处理“写事件”(零拷贝传输)
else if (key.isWritable()) {
SocketChannel channel = (SocketChannel) key.channel();
long position = 0;
long fileSize = fileChannel.size();
// 核心:零拷贝 transferTo(内核态直接传输)
while (position < fileSize) {
long transferred = fileChannel.transferTo(position, fileSize - position, channel);
position += transferred;
System.out.println("已传输:" + position + "/" + fileSize + " 字节");
}
// 传输完成,关闭资源
fileChannel.close();
channel.close();
selector.close();
System.out.println("NIO 客户端文件传输完成(零拷贝)");
return;
}
}
}
}
}2. 核心优势
- 零 CPU 拷贝:
transferTo直接让内核把文件从文件缓冲区转到 Socket 缓冲区,CPU 不用搬数据; - 线程复用:单线程处理多连接,不用为每个连接开线程;
- 非阻塞:
accept()、read()、write()都不阻塞,CPU 利用率极高。
3. 适用场景
高并发、大文件传输(比如文件服务器、Netty 网关、视频网站),是现在 Java 高并发的主流方案。
四、AIO:“全自动快递”模式
AIO(Asynchronous I/O)是“纯异步非阻塞”,通俗说就是“你下单后,不用管任何事,快递员(内核)把所有活干完,直接通知你‘搞定了’”。
1. 核心场景示例:AIO 实现“异步文件传输”
服务端(异步接收文件)
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.net.InetSocketAddress;
import java.io.FileOutputStream;
/**
* AIO 服务端:纯异步,回调通知
*/
public class AioFileServer {
public static void main(String[] args) throws Exception {
// 1. 创建异步服务端通道
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
System.out.println("AIO 服务端启动,监听 8080 端口...");
// 2. 异步监听连接(无阻塞,完成后回调)
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
try {
// 继续监听下一个连接(AIO 是一次性的,需手动再次调用)
serverChannel.accept(null, this);
System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
// 异步读取客户端数据(完成后回调)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
readData(clientChannel, buffer);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 主线程阻塞(避免程序退出)
Thread.sleep(10000);
}
// 异步读取数据
private static void readData(AsynchronousSocketChannel channel, ByteBuffer buffer) {
channel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer len, ByteBuffer buf) {
if (len > 0) {
buf.flip();
// 异步写入文件(简化:同步写入,实际可再用 AIO 异步写)
try (FileOutputStream fos = new FileOutputStream("aio_received.txt", false)) {
fos.getChannel().write(buf);
} catch (Exception e) {
e.printStackTrace();
}
buf.compact();
// 继续读取下一批数据
channel.read(buf, buf, this);
} else if (len == -1) {
try {
System.out.println("客户端断开:" + channel.getRemoteAddress());
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
exc.printStackTrace();
}
});
}
}客户端(异步发送文件)
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.net.InetSocketAddress;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* AIO 客户端:纯异步,回调通知
*/
public class AioFileClient {
public static void main(String[] args) throws Exception {
// 1. 创建异步 Socket 通道
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
// 2. 异步连接服务端(完成后回调)
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080), null, new CompletionHandler<Void, Void>() {
@Override
public void completed(Void result, Void attachment) {
System.out.println("AIO 客户端连接成功");
try {
// 3. 异步读取文件
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
Paths.get("test.txt"), StandardOpenOption.READ
);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
long position = 0;
// 异步读文件(完成后回调)
readFile(fileChannel, buffer, position, socketChannel);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 主线程阻塞(避免程序退出)
Thread.sleep(10000);
}
// 异步读文件
private static void readFile(AsynchronousFileChannel fileChannel, ByteBuffer buffer, long position, AsynchronousSocketChannel socketChannel) {
fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer len, ByteBuffer buf) {
if (len > 0) {
buf.flip();
// 异步写数据到 Socket(完成后回调)
socketChannel.write(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer writeLen, ByteBuffer b) {
if (b.hasRemaining()) {
// 未写完,继续写
socketChannel.write(b, b, this);
} else {
b.clear();
// 继续读文件下一部分
readFile(fileChannel, b, position + len, socketChannel);
}
}
@Override
public void failed(Throwable exc, ByteBuffer b) {
exc.printStackTrace();
}
});
} else {
// 文件读取完成,关闭资源
try {
fileChannel.close();
socketChannel.close();
System.out.println("AIO 客户端文件传输完成");
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
exc.printStackTrace();
}
});
}
}2. 核心特点
- 纯异步:发起请求后,程序不用管任何 I/O 过程,内核完成后通过
CompletionHandler回调通知; - 无阻塞:全程没有阻塞方法,线程利用率最高;
- 依赖 OS:Windows 用 IOCP 实现,性能最优;Linux 5.1+ 用 io_uring,旧内核会降级为 NIO 模拟。
3. 适用场景
对延迟敏感的场景(比如高频交易系统),或 Windows 环境下的高并发传输。
五、零拷贝:贯穿 NIO/AIO 的“效率神器”
零拷贝不是独立技术,是“减少数据拷贝”的优化思想,核心是“让数据在内核里直接流转,不经过用户态”。Java 提供 3 种核心实现,以下是可运行示例:
1. 实现一:transferTo/transferFrom(最常用,大文件传输首选)
原理:依赖 OS 的 sendfile 系统调用,数据直接在内核态流转,零 CPU 拷贝。
import java.nio.channels.FileChannel;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* 零拷贝示例 1:transferTo/transferFrom(文件→文件传输)
*/
public class ZeroCopyTransferExample {
public static void main(String[] args) throws Exception {
// 源文件通道(读)
FileChannel sourceChannel = new FileInputStream("source.txt").getChannel();
// 目标文件通道(写)
FileChannel targetChannel = new FileOutputStream("target.txt").getChannel();
// 核心:零拷贝传输(内核态直接复制,无 CPU 参与)
long transferred = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
System.out.println("传输完成,字节数:" + transferred);
sourceChannel.close();
targetChannel.close();
}
}2. 实现二:MappedByteBuffer(内存映射,频繁读写/随机访问)
原理:将文件映射到虚拟内存,程序直接访问内核内存,无用户态/内核态拷贝。
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.io.RandomAccessFile;
/**
* 零拷贝示例 2:MappedByteBuffer(内存映射读写文件)
*/
public class ZeroCopyMappedExample {
public static void main(String[] args) throws Exception {
// 以读写模式打开文件(RandomAccessFile 支持映射)
RandomAccessFile raf = new RandomAccessFile("mapped.txt", "rw");
FileChannel channel = raf.getChannel();
// 映射文件 0~1024 字节到虚拟内存(READ_WRITE:读写模式)
MappedByteBuffer mappedBuf = channel.map(
FileChannel.MapMode.READ_WRITE,
0,
64
);
// 直接操作映射内存(无拷贝)
mappedBuf.put(0, (byte) 'H');
mappedBuf.put(1, (byte) 'i');
mappedBuf.put(2, (byte) '!');
// 读取映射内存的数据
byte[] result = new byte[3];
mappedBuf.get(0, result);
System.out.println("读取结果:" + new String(result)); // 输出 Hi!
// 强制同步到磁盘
mappedBuf.force();
// 注意:MappedByteBuffer 无 close(),依赖 GC 释放(可通过 Unsafe 强制释放)
channel.close();
raf.close();
}
}3. 实现三:DirectByteBuffer(堆外缓冲区,NIO 网络编程)
原理:缓冲区分配在堆外内存,内核可直接访问,避免“堆内存→内核缓冲区”的 CPU 拷贝。
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;
/**
* 零拷贝示例 3:DirectByteBuffer(堆外缓冲区网络传输)
*/
public class ZeroCopyDirectBufferExample {
public static void main(String[] args) throws Exception {
// 1. 创建 Socket 通道,连接服务端(简化:假设服务端已启动)
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
// 2. 分配堆外直接缓冲区(内核可直接访问)
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024);
// 3. 写入数据到缓冲区(直接操作堆外内存)
directBuf.put("Hello Zero-Copy(DirectByteBuffer)".getBytes());
directBuf.flip();
// 4. 发送数据(无堆内存→内核拷贝)
socketChannel.write(directBuf);
System.out.println("数据发送完成(堆外缓冲区)");
directBuf.clear();
socketChannel.close();
}
}零拷贝 3 种实现对比
| 实现方式 | 核心场景 | 优点 | 缺点 |
|---|---|---|---|
| transferTo/From | 大文件传输(文件→网络/文件) | 0 次 CPU 拷贝,性能最优 | 仅支持通道间直接传输,无法修改数据 |
| MappedByteBuffer | 频繁读写小文件、大文件随机访问 | 无用户态/内核态拷贝,支持随机访问 | 映射大小限制,释放依赖 GC |
| DirectByteBuffer | NIO 网络编程、高频序列化 | 减少堆内存与内核的拷贝 | 分配/释放成本高,需注意直接内存溢出 |
六、3 大 I/O 模型对比(一张表看懂)
| 模型 | 核心特点 | 通俗比喻 | 底层依赖 | 适用场景 |
|---|---|---|---|---|
| BIO | 同步阻塞、一个连接一线程 | 傻等快递,一个快递一个人 | 无特殊依赖 | 低并发、小文件、简单场景 |
| NIO | 同步非阻塞、事件驱动 | 管家盯快递,一个人管多个 | Linux epoll/macOS kqueue | 高并发、大文件、Netty 网关 |
| AIO | 异步非阻塞、回调通知 | 全自动快递,不用自己动手 | Windows IOCP/Linux io_uring | 低延迟、Windows 环境 |
七、实际开发建议
- 优先选 NIO/Netty:日常高并发场景(网关、文件服务器)用 NIO 足够,Netty 封装了 NIO 细节,直接用就行;
- 大文件传输用
transferTo:配合DirectByteBuffer,零拷贝效率最高; - 频繁读写小文件用
MappedByteBuffer:内存映射减少拷贝,支持随机访问; - Windows 环境可试 AIO:Windows 的 IOCP 是原生异步,性能比 NIO 好;
- 小并发用 BIO:代码简单,不用折腾 NIO 的 Selector 和通道;
- Linux 系统调优:调整最大文件描述符数(
ulimit -n 65535),避免连接数限制。
八、核心总结
- I/O 优化的核心:少拷贝、少等待、多复用——减少 CPU 拷贝和上下文切换,让 CPU 专注处理业务;
- BIO 是“傻等”,NIO 是“管家盯梢”,AIO 是“全自动”;
- 零拷贝是高并发 I/O 的“关键武器”,3 种实现各有侧重,按需选择;
- 底层依赖决定性能:Linux 用 NIO(epoll),Windows 用 AIO(IOCP),选对模型才能最大化性能。
所有示例均可直接运行(需提前创建 test.txt/source.txt 等文件),建议动手跑一遍,感受不同 I/O 模型的差异~