Java 的 IO 體系經歷了從 BIO 到 NIO 再到 AIO 的演進。理解這些模型的差異和適用場景,是構建高效能網路應用的基礎。

IO 模型演進#

三種 IO 模型對比#

特性BIONIOAIO
全稱Blocking IONon-blocking IOAsynchronous IO
阻塞模式同步阻塞同步非阻塞非同步非阻塞
線程模型一連線一線程多路複用回呼機制
適用場景連線數少且固定高並行、短連線高並行、長連線
JDK 版本1.01.41.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 有三個核心概念:

  1. Channel(通道):雙向的資料傳輸通道
  2. Buffer(緩衝區):資料的臨時存儲區域
  3. 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  limit

flip() 後

[H][e][l][l][o][    ]
 ↑            ↑
 pos          limit

AIO(非同步非阻塞 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 的方法)
  • 適合大文件的隨機訪問場景

實踐建議#

  1. 小並行用 BIO:簡單直觀,開發效率高
  2. 高並行用 NIO:配合 Netty 使用
  3. 正確處理 Buffer:注意 flip()、clear() 的時機
  4. 使用 try-with-resources:確保 IO 資源正確釋放
  5. 大文件用內存映射:避免一次性加載到內存
  6. 網路編程首選 Netty:不要重複造輪子