Java 的 IO 體系經歷了從 BIO 到 NIO 再到 AIO 的演進。理解這些模型的差異和適用場景,是構建高效能網路應用的基礎。
IO 模型演進#
三種 IO 模型對比#
| 特性 | BIO | NIO | AIO |
|---|---|---|---|
| 全稱 | Blocking IO | Non-blocking IO | Asynchronous IO |
| 阻塞模式 | 同步阻塞 | 同步非阻塞 | 非同步非阻塞 |
| 線程模型 | 一連線一線程 | 多路複用 | 回呼機制 |
| 適用場景 | 連線數少且固定 | 高並行、短連線 | 高並行、長連線 |
| JDK 版本 | 1.0 | 1.4 | 1.7 |
BIO(同步阻塞 IO)#
基本模型#
flowchart LR
C1[用戶端 1] --> T1[線程 1]
C2[用戶端 2] --> T2[線程 2]
C3[用戶端 3] --> T3[線程 3]
CN[用戶端 N] --> TN[線程 N]
style T1 fill:#ffccbc
style T2 fill:#ffccbc
style T3 fill:#ffccbc
style TN fill:#ffccbc// BIO 服務端示例
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待連線
new Thread(() -> {
try (InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream()) {
byte[] buffer = new byte[1024];
int len = in.read(buffer); // 阻塞等待資料
// 處理請求
out.write(response);
}
}).start();
}BIO 模型的問題:
- 每個連線都需要獨立線程處理
- 線程資源寶貴,無法支撐大量並行連線
- 線程阻塞期間資源被佔用卻無法做其他事
NIO(同步非阻塞 IO)#
核心組件#
NIO 有三個核心概念:
- Channel(通道):雙向的資料傳輸通道
- Buffer(緩衝區):資料的臨時存儲區域
- Selector(選擇器):多路複用器,監控多個 Channel
多路複用模型#
flowchart LR
C1[用戶端 1] -->|Channel| S["Selector<br/>(多路複用器)"]
C2[用戶端 2] -->|Channel| S
C3[用戶端 3] -->|Channel| S
S --> T[單線程處理]
style S fill:#fff9c4
style T fill:#c8e6c9// NIO 服務端示例
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false); // 非阻塞模式
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞直到有事件就緒
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
if (key.isAcceptable()) {
// 處理新連線
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 處理讀事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
// 處理資料
}
it.remove();
}
}Buffer 操作#
Buffer 有四個核心屬性:
| 屬性 | 說明 |
|---|---|
| capacity | 容量,不可變 |
| position | 當前位置 |
| limit | 讀/寫的邊界 |
| mark | 標記位置 |
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 寫入資料
buffer.put("Hello".getBytes());
// 切換為讀模式
buffer.flip();
// 讀取資料
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
// 清空緩衝區
buffer.clear();Buffer 狀態變化圖解
初始狀態:
position=0, limit=capacity
[ ]
↑ ↑
position limit/capacity寫入 5 字節後:
[H][e][l][l][o][ ]
↑ ↑
pos limitflip() 後:
[H][e][l][l][o][ ]
↑ ↑
pos limitAIO(非同步非阻塞 IO)#
AIO 基於事件和回呼機制,由操作系統完成 IO 操作後通知應用程序:
// AIO 服務端示例
AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// 繼續接受下一個連線
server.accept(null, this);
// 處理當前連線
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// 處理讀取的資料
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
// 處理失敗
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
// 處理失敗
}
});AIO 在 Linux 上的實現實際上是基於 epoll 的模擬,並非真正的非同步 IO。在 Windows 上通過 IOCP 實現真正的非同步。因此在 Linux 環境下,NIO 通常是更好的選擇。
IO 模型選擇#
決策流程#
flowchart TD
Q1{連線數多嗎?}
Q1 -->|Yes| Q2{需要高吞吐嗎?}
Q1 -->|No| BIO["BIO 即可"]
Q2 -->|Yes| NIO["NIO (Netty)"]
Q2 -->|No| AIO["可以考慮 AIO"]
style BIO fill:#c8e6c9
style NIO fill:#bbdefb
style AIO fill:#fff9c4各場景推薦#
| 場景 | 推薦方案 |
|---|---|
| 傳統 Web 應用 | BIO + 線程池(Servlet 容器) |
| 高並行網關 | NIO(Netty) |
| 即時通訊 | NIO(Netty) |
| 文件傳輸 | NIO 或 AIO |
Netty 簡介#
Netty 是對 NIO 的高級封裝,解決了原生 NIO 的諸多問題:
- 簡化了 API 的使用
- 解決了空輪詢 Bug
- 提供了豐富的編解碼器
- 內建零拷貝支持
Netty 核心組件#
flowchart TD
subgraph ELG["EventLoopGroup"]
EL1[EventLoop]
EL2[EventLoop]
EL3[EventLoop]
end
subgraph CH["Channel"]
subgraph CP["ChannelPipeline"]
H1[Handler] --> H2[Handler] --> H3[...]
end
end
ELG --> CH
style ELG fill:#e3f2fd
style CH fill:#fff3e0
style CP fill:#fff9c4// Netty 服務端示例
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new MyHandler());
}
});
ChannelFuture f = b.bind(8080).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}在生產環境中,強烈建議使用 Netty 而非原生 NIO。Netty 經過大量實踐驗證,穩定可靠。
文件 IO#
傳統文件讀寫#
// 使用 try-with-resources 確保資源釋放
try (FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}NIO 文件操作#
// 使用 Files 工具類(Java 7+)
Path path = Paths.get("file.txt");
// 讀取所有行
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// 寫入
Files.write(path, lines, StandardCharsets.UTF_8);
// 複製
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);內存映射文件#
適用於大文件處理:
try (RandomAccessFile file = new RandomAccessFile("large.dat", "rw");
FileChannel channel = file.getChannel()) {
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, channel.size());
// 直接操作內存,由操作系統負責刷盤
buffer.putInt(0, 12345);
}內存映射文件雖然高效,但要注意:
- 映射的內存不受 JVM 堆大小限制
- 需要手動管理(Java 沒有提供直接 unmap 的方法)
- 適合大文件的隨機訪問場景
實踐建議#
- 小並行用 BIO:簡單直觀,開發效率高
- 高並行用 NIO:配合 Netty 使用
- 正確處理 Buffer:注意 flip()、clear() 的時機
- 使用 try-with-resources:確保 IO 資源正確釋放
- 大文件用內存映射:避免一次性加載到內存
- 網路編程首選 Netty:不要重複造輪子