Lesson 48 · Java 高级特性

IO 模型:BIO、NIO 与 Reactor 模式

高级·#Java·#IO·#网络

第 1 站

BIO:一个连接一个线程,简单但昂贵

BIO(Blocking IO)是 JDK 1.0 就有的传统 IO 模型。核心特征:线程在读写数据时会阻塞,直到操作完成。最典型的编程模式是"一个连接一个线程"。

BioServer.java · 传统 BIO 服务器
public class BioServer {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(8080);
        System.out.println("BIO Server 启动,端口 8080");

        while (true) {
            // ★ accept() 会阻塞,直到有客户端连接
            Socket socket = server.accept();

            // ★ 每个连接必须启动一个新线程处理
            new Thread(() -> {
                try (InputStream in = socket.getInputStream();
                     OutputStream out = socket.getOutputStream()) {

                    byte[] buf = new byte[1024];
                    int len;
                    // ★ read() 会阻塞,直到有数据可读
                    while ((len = in.read(buf)) != -1) {
                        out.write(buf, 0, len);  // echo 回写
                        out.flush();
                    }
                } catch (Exception e) { e.printStackTrace(); }
            }).start();
        }
    }
}

BIO 的核心问题:线程数量与连接数量 1:1 绑定。1 万个连接就需要 1 万个线程,每个线程栈默认占 1MB 内存,光线程栈就要 10GB。更严重的是上下文切换开销——操作系统在 1 万个线程间调度,CPU 时间大量浪费在切换而非计算上。

BIO 的两处阻塞

阻塞点 1:accept()——没有新连接时,主线程卡在这里。

阻塞点 2:read()/write()——没有数据可读/缓冲区满时,工作线程卡在这里。

两处阻塞意味着:即使客户端连接建立后不发送任何数据,为其分配的线程也只能干等,无法服务其他连接。

第 2 站

NIO 三件套:Channel、Buffer、Selector

JDK 1.4 引入 NIO(New IO / Non-blocking IO),核心是三个组件:Channel(通道)、Buffer(缓冲区)、Selector(多路复用器)。它们共同实现了一个线程处理多个连接。

组件BIO 对应NIO 角色
ChannelStream双向通道,支持读写。核心实现:SocketChannelServerSocketChannelFileChannel
Bufferbyte[]结构化缓冲区,通过 position/limit/capacity 精确控制读写位置。核心:ByteBuffer
SelectorIO 多路复用器,一个线程监控多个 Channel 的事件(连接、读、写)
NioServer.java · NIO 服务器基本结构
public class NioServer {
    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();

        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false);  // ★ 非阻塞模式

        // 将 ServerChannel 注册到 Selector,监听 ACCEPT 事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        while (true) {
            // ★ select() 阻塞直到有事件就绪
            selector.select();

            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();

            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();  // ★ 必须移除,否则下次会重复处理

                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();
                    buffer.clear();
                    int len = client.read(buffer);  // ★ 非阻塞,可能返回 0
                    if (len > 0) {
                        buffer.flip();  // ★ 切换为读模式
                        client.write(buffer);
                    }
                }
            }
        }
    }
}
BufferMechanism.java · Buffer 的 position/limit/capacity
ByteBuffer buf = ByteBuffer.allocate(10);
// capacity=10, position=0, limit=10

// 写入 "Hello"(5 个字节)
buf.put("Hello".getBytes());
// capacity=10, position=5, limit=10

// flip():准备读取——limit 设为当前 position,position 归零
buf.flip();
// capacity=10, position=0, limit=5

// 读取 5 字节
byte[] data = new byte[buf.remaining()];  // remaining = limit - position = 5
buf.get(data);
// capacity=10, position=5, limit=5

// clear():重置为写模式
buf.clear();
// capacity=10, position=0, limit=10(数据还在,但 position 重置了)

// compact():丢弃已读数据,保留未读数据
buf.compact();
// 将未读数据移到缓冲区开头,position 紧跟其后
为什么 NIO 的 read() 是非阻塞的?那 read() 返回 0 怎么办?

非阻塞模式下,SocketChannel.read() 如果没有数据可读,会立即返回 0 而不是等待。所以代码中必须检查 len > 0 再处理数据。配合 Selector,只有当 Channel 确实有数据可读时(OP_READ 事件就绪),才调用 read(),这样就避免了"忙轮询"浪费 CPU。
第 3 站

IO 多路复用:select / poll / epoll

Selector 的底层实现依赖操作系统的 IO 多路复用机制。理解 select、poll、epoll 三者的区别,是理解 NIO 性能的关键。

select / poll / epoll 对比 select fd_set 位图(固定 1024) 每次调用都要拷贝全部 fd 到内核 内核遍历所有 fd 检查就绪 O(n) 遍历 + 返回后需再次遍历 fd 返回后位图被清空,需重新设置 上限 1024 个 fd poll pollfd 数组(无上限) 用 revents 字段标记就绪 返回后不需要重新设置 fd 列表 O(n) 遍历(仍需全量扫描) 每次仍需用户态→内核态拷贝 无 fd 数量限制 epoll(Linux) 红黑树 + 就绪链表 就绪链表(只包含活跃 fd) epoll_create → 内核创建实例 epoll_ctl → 增删改 fd(mmap 共享) O(1) 事件通知(回调机制) epoll_wait → 只返回就绪 fd 无 fd 上限,内存共享 核心差异对比 性能维度 select poll epoll 时间复杂度 O(n) O(n) O(1) fd 上限 1024 数据拷贝 每次全量拷贝 每次全量拷贝 mmap 共享 触发方式 水平触发 水平触发 水平 + 边缘触发 适用场景 连接少且活跃 跨平台兼容 大量连接,少量活跃
图 1select / poll / epoll 对比:epoll 通过回调机制实现 O(1) 事件通知,配合 mmap 避免数据拷贝,是 Linux 上高性能网络编程的基石
epoll 的核心优势:回调 + 共享内存

1. epoll_create() 在内核创建一个事件表(红黑树),通过 mmap 与用户空间共享内存
2. epoll_ctl() 注册 fd 时,内核为该 fd 设置一个回调函数——当该 fd 就绪时,回调将其加入就绪链表
3. epoll_wait() 只需观察就绪链表,只返回有事件的 fd,无需遍历全部 fd
epoll 一定是最好的吗?连接数很少时呢?

当连接数很少(比如 < 100)且都很活跃时,epoll 的优势不明显,select/poll 的简单实现反而可能更快。epoll 的真正价值在于大量连接中只有少量活跃的场景——这正是 Web 服务器的典型负载。
第 4 站

Reactor 模式:事件驱动的服务端架构

Reactor 模式是基于 IO 多路复用的事件驱动架构。核心思想:所有 IO 操作都通过事件注册和回调完成,主线程(Reactor)只负责监听事件和分发任务。

Reactor 模式有三种经典实现:

ReactorPatterns.java · 三种 Reactor 模式对比
/* ═══════════ 模型一:单线程 Reactor ═══════════
 *
 *   Acceptor/Reader/Writer 全部在一个线程中
 *
 *   [Reactor Thread]
 *     ├── select() 监听事件
 *     ├── accept() 处理连接
 *     ├── read()   读取数据
 *     ├── process() 业务处理 ← 阻塞在这里会影响其他事件!
 *     └── write()  写回响应
 *
 *   优点:简单,无线程切换开销
 *   缺点:业务处理耗时会阻塞整个 Reactor
 *   适用:业务逻辑简单、处理速度快的场景
 */

/* ═══════════ 模型二:多线程 Reactor(一主多从)═══════════
 *
 *   Reactor 线程只负责 IO,业务处理交给 Worker 线程池
 *
 *   [Reactor Thread]            [Worker Pool]
 *     ├── select()                ├── Thread-1: process()
 *     ├── accept()                ├── Thread-2: process()
 *     ├── read()                  └── Thread-3: process()
 *     └── write() ←── 回调
 *
 *   优点:IO 和业务分离,互不阻塞
 *   缺点:单 Reactor 仍是连接数瓶颈(百万连接时 select 压力大)
 *   适用:中等并发,业务处理较重的场景
 */

/* ═══════════ 模型三:主从 Reactor(多线程 + 多 Reactor)═══════════
 *
 *   MainReactor 负责 accept,SubReactor 负责读写
 *
 *   [MainReactor]
 *     └── accept() → 分配给 SubReactor
 *
 *   [SubReactor-1]     [SubReactor-2]     [SubReactor-N]
 *     ├── select()       ├── select()       ├── select()
 *     ├── read()         ├── read()         ├── read()
 *     └── write()        └── write()        └── write()
 *          ↓                  ↓                  ↓
 *   [Worker Pool](如果业务处理也很重)
 *
 *   优点:连接建立和 IO 读写分离,支持百万连接
 *   缺点:实现复杂
 *   适用:高并发、高性能场景(Netty 采用此模型)
 */
模型Reactor 数量业务处理连接能力代表框架
单线程1同线程数千Redis(单线程版)
多线程1Worker 池数万早期 Mina
主从N+1Worker 池数十万+Netty、Nginx
第 5 站

Netty 架构:主从 Reactor 的工业级实现

Netty 是 Java 生态最流行的高性能网络框架,底层就是主从 Reactor + NIO。理解 Netty 的线程模型,就是理解 Reactor 模式的落地。

NettyServer.java · Netty 服务器启动代码
public class NettyServer {
    public void start() throws Exception {
        // BossGroup:MainReactor,负责 accept 新连接(通常 1 个线程)
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);

        // WorkerGroup:SubReactor,负责已连接 Channel 的 IO 读写(默认 CPU*2 线程)
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) {
                    ChannelPipeline p = ch.pipeline();
                    p.addLast(new Decoder());       // 解码器
                    p.addLast(new Encoder());       // 编码器
                    p.addLast(new BusinessHandler()); // 业务逻辑
                }
            })
            .option(ChannelOption.SO_BACKLOG, 1024)   // 连接队列大小
            .childOption(ChannelOption.SO_KEEPALIVE, true);

        // 绑定端口,异步启动
        ChannelFuture future = bootstrap.bind(8080).sync();
        future.channel().closeFuture().sync();

        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

Netty 线程模型的关键要点:

BossGroup 为什么通常只配 1 个线程?

accept 操作的频率远低于读写操作。一个端口上每秒能 accept 的连接数有限(受 TCP 三次握手和 backlog 队列限制),1 个线程绑死一个端口完全够用。如果有多个端口监听,可以配置多个 Boss 线程。

WorkerGroup 默认为什么是 CPU*2 个线程?

每个 NioEventLoop 线程绑定一个 Selector,负责一批 Channel 的 IO 操作。CPU*2 是一个经验值——在保证充分利用 CPU 的同时,避免过多的上下文切换。对于 IO 密集型场景可以适当增大。

NettyPipeline.java · ChannelPipeline 责任链
/*
 * Netty 的 ChannelPipeline 是一条 Handler 链(责任链模式):
 *
 *  Inbound:  网络数据 → Decoder → Handler1 → Handler2 → ...
 *  Outbound: ... ← Handler2 ← Handler1 ← Encoder ← 业务写入
 *
 * 关键设计:
 * ① 每个 Channel 有独立的 Pipeline,线程安全
 * ② 同一个 Channel 的所有操作都在同一个 EventLoop 线程执行,无需加锁
 * ③ 如果 Handler 中有耗时操作,必须提交到独立的业务线程池
 */

public class BusinessHandler extends SimpleChannelInboundHandler<String> {
    // ★ 业务线程池——耗时操作不在 EventLoop 中执行
    private static final EventExecutorGroup bizGroup =
        new DefaultEventExecutorGroup(16);

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        // 如果业务处理很快,直接在 EventLoop 线程执行即可
        // 如果涉及 DB 查询、RPC 调用等耗时操作,必须异步
        ctx.writeAndFlush("Echo: " + msg);
    }
}

// 或者在 pipeline 中指定业务线程池:
pipeline.addLast(bizGroup, "biz", new BusinessHandler());
Netty 高性能的核心设计:

1. 零拷贝:CompositeByteBuf 逻辑组合多个 Buffer,避免内存拷贝
2. 无锁串行化:同一 Channel 的所有操作在同一 EventLoop 线程执行,无需加锁
3. 内存池:ByteBuf 对象池化复用,减少 GC 压力
4. 直接内存:DirectByteBuffer 避免 JVM 堆与系统内存之间的拷贝
第 6 站

AIO / NIO.2:异步 IO 的理想与现实

JDK 7 引入了 NIO.2(也叫 AIO),提供真正的异步 IO:发起读写操作后立即返回,操作系统完成后通过回调通知。与 NIO 的"非阻塞 + 轮询事件"不同,AIO 是"发起操作 + 完成回调"。

AioServer.java · AIO 异步服务器
public class AioServer {
    public static void main(String[] args) throws Exception {
        AsynchronousServerSocketChannel server =
            AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080));

        System.out.println("AIO Server 启动");

        // ★ 异步 accept:发起后立即返回,连接到达时回调 completed()
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel client, Void att) {
                // 继续接受下一个连接
                server.accept(null, this);

                ByteBuffer buf = ByteBuffer.allocate(1024);
                // ★ 异步 read:发起后立即返回,数据就绪时回调
                client.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer len, ByteBuffer buffer) {
                        buffer.flip();
                        client.write(buffer);
                    }
                    @Override
                    public void failed(Throwable exc, ByteBuffer buffer) { }
                });
            }
            @Override
            public void failed(Throwable exc, Void att) { }
        });

        System.in.read(); // 主线程阻塞等待
    }
}
特性BIONIOAIO (NIO.2)
IO 模型同步阻塞同步非阻塞(多路复用)异步非阻塞
编程模型每连接一线程事件驱动 + 回调Future / CompletionHandler
线程模型线程池 per connectionReactor(少量线程)OS 线程池回调
适用场景连接少、短连接大量长连接(通用)大量长连接 + 重 IO
Linux 支持良好epoll 优秀io_uring(内核 5.1+)
生态成熟度高(Netty)低(Java AIO 几乎没人用)
为什么 Java AIO 在生产中几乎没人用?

Linux 上的 AIO 实现不成熟。Linux 内核的 AIO(libaio)仅支持文件 IO,不支持 Socket IO。Java 的 AsynchronousSocketChannel 在 Linux 上实际是用 epoll 模拟的——和 NIO 本质上一样,但多了一层封装,性能反而不如直接用 NIO。

在 Windows 上,AIO 使用 IOCP(IO Completion Port),性能很好。但服务端主要跑在 Linux 上,所以 Netty 选择了 NIO + epoll 的路线。

Linux 5.1+ 引入的 io_uring 是真正的高性能异步 IO 接口,但 Java 原生还不支持。

第 7 站

总结:IO 模型全景与面试高频问题

维度BIONIOAIO
同步/异步同步同步异步
阻塞/非阻塞阻塞非阻塞非阻塞
编程复杂度
并发能力低(千级)高(十万级)高(理论百万级)
JDK 版本1.01.47
代表框架JDK 原生Netty、Mina少有人用
面试高频问题速记
  • "同步和异步的区别?"——同步:调用者等待结果返回;异步:调用后立即返回,通过回调/Future 获取结果
  • "阻塞和非阻塞的区别?"——阻塞:线程在 IO 操作期间被挂起;非阻塞:IO 操作立即返回(可能没数据)
  • "NIO 比 BIO 好在哪?"——BIO 一线程一连接,NIO 一线程多连接(Selector 多路复用),大幅减少线程数和上下文切换
  • "epoll 为什么快?"——事件回调 O(1) 通知 + mmap 共享内存 + 只返回就绪 fd
  • "Reactor 模式是什么?"——基于 IO 多路复用的事件驱动架构,主从 Reactor 将 accept 和 IO 读写分离
  • "Netty 为什么高性能?"——主从 Reactor + 零拷贝 + 无锁串行化 + 内存池 + DirectBuffer

面试回答模板:

"Java IO 模型分为 BIO、NIO、AIO 三种。BIO 是同步阻塞,一个连接对应一个线程,简单但并发能力低。NIO 基于 Selector 实现 IO 多路复用,一个线程可以监控多个 Channel 的事件,底层在 Linux 上使用 epoll 实现 O(1) 的事件通知。Reactor 模式是基于 NIO 的事件驱动架构,Netty 采用主从 Reactor 模型:BossGroup 负责 accept 连接,WorkerGroup 负责 IO 读写,配合 ChannelPipeline 责任链、零拷贝和内存池实现了高性能。AIO 虽然是异步非阻塞,但在 Linux 上实现不成熟,生产中基本用 NIO + Netty。"