小陈的知识图谱
Java 基础L4 高级重要

IO/NIO 模型

BIO/NIO/AIO、零拷贝、Reactor 模式

IO 模型概述

Java 中的 IO 模型发展经历了从 BIO → NIO → AIO 的演进,本质是操作系统 IO 模型在 Java 层面的封装。

阻塞IO (BIO)       非阻塞IO (NIO)        异步IO (AIO)
  Java 1.0            Java 1.4              Java 1.7
      │                    │                     │
      ▼                    ▼                     ▼
  同步阻塞 I/O         同步非阻塞 I/O         异步非阻塞 I/O
  线程阻塞等待数据      轮询检查就绪状态      数据就绪后回调通知

三种 IO 模型对比

维度BIO(Blocking IO)NIO(Non-blocking IO)AIO(Asynchronous IO)
阻塞线程阻塞在 read/write不阻塞,轮询 Selector不阻塞,回调完成
线程模型一连接一线程一个 Selector 管理多 Channel提交后回调
并发支持差(线程数 = 连接数)好(少量线程管理大量连接)极好(回调驱动)
适用场景连接数少、固定架构连接数多、短连接连接数多、长连接
JDK 版本1.0+1.4+1.7+
延迟高(线程上下文切换)低(事件驱动)最低(回调)

BIO 示例

// 传统 BIO — 每连接一个线程
public class BioServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        while (true) {
            Socket socket = serverSocket.accept(); // 阻塞
            new Thread(() -> {
                try {
                    BufferedReader reader = new BufferedReader(
                        new InputStreamReader(socket.getInputStream()));
                    PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
                    String line;
                    while ((line = reader.readLine()) != null) {
                        writer.println("Echo: " + line);
                    }
                } catch (IOException e) { e.printStackTrace(); }
            }).start();
        }
    }
}

BIO 的问题:每个连接一个线程,1000 个连接就需要 1000 个线程,线程创建/切换开销巨大。

NIO 三大核心组件

NIO 基于事件驱动,由 Channel、Buffer、Selector 三大核心组件构成。

Selector(选择器)
                      ┌──────────┐
                      │          │
                      │ select() │  ← 单线程轮询就绪事件
                      │    │     │
                      └────┼─────┘
                           │
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
      ┌──────┐        ┌──────┐        ┌──────┐
      │Channel│        │Channel│        │Channel│  ← 双向通道
      ├──────┤        ├──────┤        ├──────┤
      │Buffer│        │Buffer│        │Buffer│   ← 数据缓冲区
      └──────┘        └──────┘        └──────┘

Channel(通道)

与流(Stream)不同,Channel 是双向的,既可以读也可以写。

// 主要 Channel 类型
FileChannel              // 文件 IO
SocketChannel            // TCP 客户端
ServerSocketChannel      // TCP 服务端
DatagramChannel          // UDP

Buffer(缓冲区)

Buffer 是 NIO 读写数据的载体,包含三个核心指针:

position         limit         capacity
         │               │              │
         ▼               ▼              ▼
    ┌────┼───────────────┼──────────────┼────┐
    │  0 │  1 │ 2 │ ... │  未读  │ ...  │ N  │
    └────┴────┴────┴────┴────────┴──────┴────┘
    ←—— 已读/已写区 ——→ ←—— 可读写区 ———→

核心方法

方法效果
flip()limit = position; position = 0 写→读切换
clear()position = 0; limit = capacity 清空缓冲区
compact()压缩未读数据到头部
rewind()position = 0 重新读
remaining()limit - position 剩余可读数量

// NIO Buffer 典型操作
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 写入数据
buffer.put("Hello".getBytes());

// 切换为读模式
buffer.flip();

// 读取数据
byte[] dst = new byte[buffer.remaining()];
buffer.get(dst);

// 清空
buffer.clear();

// 或压缩(保留未读数据)
// buffer.compact();

Selector(选择器)

Selector 是 NIO 事件驱动的核心,通过一个线程管理多个 Channel。

可监听的事件

事件说明
OP_ACCEPT16ServerSocketChannel 有新的连接
OP_CONNECT8SocketChannel 连接建立
OP_READ1通道中有数据可读
OP_WRITE4通道可以写入数据

// NIO Selector 完整示例
public class NioServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);  // 必须非阻塞
        ssc.bind(new InetSocketAddress(8080));
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            selector.select();  // 阻塞,直到有事件就绪
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();

                if (key.isAcceptable()) {
                    // 接受新连接
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 读取数据
                    SocketChannel sc = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = sc.read(buffer);
                    if (len > 0) {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        System.out.println("Received: " + new String(data));
                    }
                }
            }
        }
    }
}

Reactor 线程模型

Reactor 模式是 NIO 的最佳实践,Netty 等框架的核心设计模式。

单 Reactor 单线程

Reactor Thread
    ┌────────────────────────────────┐
    │           Selector             │
    │  ┌──────┐  ┌──────┐  ┌──────┐ │
    │  │accept│  │ read │  │write │ │
    │  └──────┘  └──────┘  └──────┘ │
    └───────────────┬────────────────┘
                    │
        ┌───────────┼───────────┐
        │           │           │
        ▼           ▼           ▼
    ┌──────┐   ┌──────┐   ┌──────┐
    │Handler│   │Handler│   │Handler│  ← 业务处理也在 Reactor 线程
    └──────┘   └──────┘   └──────┘

  • 所有 IO 和业务逻辑都在一个线程
  • 适用于 CPU 密集型、连接数少的场景。
  • 缺点:业务逻辑阻塞时,整个服务阻塞。

单 Reactor 多线程

Reactor Thread
    ┌────────────────────────────────┐
    │           Selector             │
    │  ┌──────┐  ┌──────┐  ┌──────┐ │
    │  │accept│  │ read │  │write │ │
    │  └──────┘  └──────┘  └──────┘ │
    └───────────────┬────────────────┘
                    │ 仅处理 IO 事件
                    ▼
    ┌──────────────────────────────────┐
    │       Worker 线程池               │
    │  ┌──────┐ ┌──────┐ ┌──────┐     │ ← 业务逻辑在线程池中异步处理
    │  │Worker│ │Worker│ │Worker│     │
    │  └──────┘ └──────┘ └──────┘     │
    └──────────────────────────────────┘

  • Reactor 线程负责所有 IO 事件分发。
  • Worker 线程池处理业务逻辑。
  • 缺点:Reactor 线程仍可能成为瓶颈。

主从 Reactor 多线程(Netty 模型)

MainReactor Group (BOSS)
    ┌──────────────────────────────┐
    │   Selector (处理 accept)      │
    │   ┌──────┐  ┌──────┐       │
    │   │accept│  │accept│       │
    │   └──────┘  └──────┘       │
    └──────────────┬───────────────┘
                   │ 分发已接受的 Channel
                   ▼
    SubReactor Group (WORKER)
    ┌──────────────────────────────┐
    │   Selector (处理 read/write) │
    │   ┌─────┐ ┌─────┐ ┌─────┐  │
    │   │read │ │write│ │read │  │
    │   └─────┘ └─────┘ └─────┘  │
    └──────────────┬───────────────┘
                   │ 需要时派发
                   ▼
    ┌──────────────────────────────┐
    │  业务线程池 (ChannelHandler) │
    └──────────────────────────────┘

  • MainReactor 负责 accept 新连接(BOSS Group)。
  • SubReactor 负责已连接 Channel 的 read/write(WORKER Group)。
  • 业务处理在 Handler 中异步执行,也可交给业务线程池。
  • Netty 采用此模型,可根据 CPU 核数设置多组 SubReactor。

零拷贝技术(Zero-Copy)

传统 IO 的 4 次拷贝

磁盘 → 内核缓冲区 (DMA 拷贝)
  内核缓冲区 → 用户缓冲区 (CPU 拷贝)
  用户缓冲区 → Socket 缓冲区 (CPU 拷贝)
  Socket 缓冲区 → 网卡 (DMA 拷贝)

// 传统读-写(4 次拷贝 + 2 次系统调用 + 3 次上下文切换)
File.read(file, buf, len);
Socket.send(socket, buf, len);

mmap(内存映射)

磁盘 → 内核缓冲区 (DMA 拷贝)
  内核缓冲区 → 用户空间共享 (无需拷贝,通过缺页中断映射)
  内核缓冲区 → Socket 缓冲区 (CPU 拷贝)
  Socket 缓冲区 → 网卡 (DMA 拷贝)

  共 3 次拷贝(比传统少 1 次)

FileChannel fileChannel = new RandomAccessFile("file.txt", "rw").getChannel();
MappedByteBuffer mappedBuffer = fileChannel.map(
    FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
// 直接操作 mappedBuffer 即操作文件,无需 read/write

sendfile(零拷贝)

磁盘 → 内核缓冲区 (DMA 拷贝)
  内核缓冲区 → 网卡 (DMA 拷贝,由 SG-DMA 引擎直接拷贝)

  共 2 次拷贝(完全在内核态完成,0 次 CPU 拷贝)

// Linux 2.4+ 真正零拷贝
FileChannel fileChannel = FileChannel.open(Paths.get("file.txt"));
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
// 底层调用 sendfile 系统调用

Java 中的零拷贝总结

技术CPU 拷贝次数DMA 拷贝次数系统调用适用场景
传统 IO22read + write通用
mmap12mmap + write文件读写频繁
sendfile02sendfile文件传输(如静态文件服务)
DirectBuffer0N/A(堆外直接使用)N/A网络 IO 频繁

Netty 架构设计

Netty 核心组件

Netty 架构
    ┌──────────────────────────────────────────┐
    │              Bootstrap                   │
    │   ┌─────────────────────────────────┐   │
    │   │        EventLoopGroup           │   │
    │   │   ┌─────────┐  ┌─────────┐     │   │
    │   │   │EventLoop│  │EventLoop│ ... │   │
    │   │   │ ┌─────┐ │  │ ┌─────┐ │     │   │
    │   │   │ │Selector│ │  │ │Selector│ │     │   │
    │   │   │ └─────┘ │  │ └─────┘ │     │   │
    │   │   └─────────┘  └─────────┘     │   │
    │   └─────────────────────────────────┘   │
    │                                         │
    │   Channel → ChannelPipeline             │
    │              ┌─────────────────┐        │
    │              │ ChannelHandler  │        │
    │              │ ┌─────────────┐│        │
    │              │ │ Encoder     ││        │
    │              │ │ Decoder     ││        │
    │              │ │ Business    ││        │
    │              │ └─────────────┘│        │
    │              └─────────────────┘        │
    └──────────────────────────────────────────┘

核心优势

1. ✅ API 简单易用,相比原生 NIO 代码量减少 80%。

2. ✅ 内置编解码器(Protobuf/JSON/HTTP)。

3. ✅ 零拷贝:CompositeByteBuf、FileRegion。

4. ✅ 内存池:ByteBuf 池化,避免 GC 压力。

5. ✅ 高并发:主从 Reactor 模型。

6. ✅ 可扩展:Pipeline 编排 Handler 链。

Netty 简单示例

// 服务端
EventLoopGroup bossGroup = new NioEventLoopGroup(1);      // MainReactor
EventLoopGroup workerGroup = new NioEventLoopGroup();     // SubReactor

try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
     .channel(NioServerSocketChannel.class)
     .childHandler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) {
             ch.pipeline().addLast(
                 new StringDecoder(),
                 new StringEncoder(),
                 new SimpleChannelInboundHandler<String>() {
                     @Override
                     protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                         System.out.println("收到: " + msg);
                         ctx.writeAndFlush("回复: " + msg);
                     }
                 }
             );
         }
     })
     .option(ChannelOption.SO_BACKLOG, 128)
     .childOption(ChannelOption.SO_KEEPALIVE, true);

    ChannelFuture f = b.bind(8080).sync();
    f.channel().closeFuture().sync();
} finally {
    workerGroup.shutdownGracefully();
    bossGroup.shutdownGracefully();
}

面试高频 Q&A

Q1: BIO、NIO、AIO 的区别?

  • BIO:同步阻塞,一连接一线程。
  • NIO:同步非阻塞,Selector 多路复用,事件驱动。
  • AIO:异步非阻塞,数据就绪后回调。
  • 实际工程中 NIO(Netty)使用最广泛,AIO 在 Linux 上性能有限。

Q2: NIO 的空轮询问题(JDK 1.8 之前)?

  • Linux 下 epoll 的 bug 导致 Selector.select() 即使没有就绪事件也立即返回,导致 CPU 100%。
  • 解决方案:记录 select 返回时间,若短时间内空转多次,重建 Selector 重新注册 Channel。
  • Netty 内部已处理此问题(RebuildSelector)。

Q3: 零拷贝解决了什么问题?

  • 减少数据在内核态和用户态之间的拷贝次数。
  • 减少 CPU 开销和上下文切换。
  • 提升 IO 吞吐量。

Q4: Netty 如何解决 TCP 粘包/拆包问题?

  • 行解码器:LineBasedFrameDecoder(按换行符分割)。
  • 定长解码器:FixedLengthFrameDecoder。
  • 长度域解码器:LengthFieldBasedFrameDecoder(如 Length + Body)。
  • 自定义:继承 ByteToMessageDecoder 自行实现。

Q5: mmap 和 sendfile 有什么区别?

  • mmap:将文件映射到进程地址空间,用户态可直接操作文件数据,支持随机读写。
  • sendfile:在内核态完成数据传输,用户态不可读数据,适合大文件传输。

---

核心要点

  • BIO/NIO/AIO 区别
  • NIO 三大核心组件
  • Reactor 线程模型
  • 零拷贝技术原理
  • Netty 架构设计