Lesson 48 · Java 高级特性
IO 模型:BIO、NIO 与 Reactor 模式
BIO:一个连接一个线程,简单但昂贵
BIO(Blocking IO)是 JDK 1.0 就有的传统 IO 模型。核心特征:线程在读写数据时会阻塞,直到操作完成。最典型的编程模式是"一个连接一个线程"。
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 时间大量浪费在切换而非计算上。
阻塞点 1:accept()——没有新连接时,主线程卡在这里。
阻塞点 2:read()/write()——没有数据可读/缓冲区满时,工作线程卡在这里。
两处阻塞意味着:即使客户端连接建立后不发送任何数据,为其分配的线程也只能干等,无法服务其他连接。
NIO 三件套:Channel、Buffer、Selector
JDK 1.4 引入 NIO(New IO / Non-blocking IO),核心是三个组件:Channel(通道)、Buffer(缓冲区)、Selector(多路复用器)。它们共同实现了一个线程处理多个连接。
| 组件 | BIO 对应 | NIO 角色 |
|---|---|---|
| Channel | Stream | 双向通道,支持读写。核心实现:SocketChannel、ServerSocketChannel、FileChannel |
| Buffer | byte[] | 结构化缓冲区,通过 position/limit/capacity 精确控制读写位置。核心:ByteBuffer |
| Selector | 无 | IO 多路复用器,一个线程监控多个 Channel 的事件(连接、读、写) |
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);
}
}
}
}
}
}
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 紧跟其后
非阻塞模式下,
SocketChannel.read() 如果没有数据可读,会立即返回 0 而不是等待。所以代码中必须检查 len > 0 再处理数据。配合 Selector,只有当 Channel 确实有数据可读时(OP_READ 事件就绪),才调用 read(),这样就避免了"忙轮询"浪费 CPU。IO 多路复用:select / poll / epoll
Selector 的底层实现依赖操作系统的 IO 多路复用机制。理解 select、poll、epoll 三者的区别,是理解 NIO 性能的关键。
1.
epoll_create() 在内核创建一个事件表(红黑树),通过 mmap 与用户空间共享内存2.
epoll_ctl() 注册 fd 时,内核为该 fd 设置一个回调函数——当该 fd 就绪时,回调将其加入就绪链表3.
epoll_wait() 只需观察就绪链表,只返回有事件的 fd,无需遍历全部 fd
当连接数很少(比如 < 100)且都很活跃时,epoll 的优势不明显,select/poll 的简单实现反而可能更快。epoll 的真正价值在于大量连接中只有少量活跃的场景——这正是 Web 服务器的典型负载。
Reactor 模式:事件驱动的服务端架构
Reactor 模式是基于 IO 多路复用的事件驱动架构。核心思想:所有 IO 操作都通过事件注册和回调完成,主线程(Reactor)只负责监听事件和分发任务。
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(单线程版) |
| 多线程 | 1 | Worker 池 | 数万 | 早期 Mina |
| 主从 | N+1 | Worker 池 | 数十万+ | Netty、Nginx |
Netty 架构:主从 Reactor 的工业级实现
Netty 是 Java 生态最流行的高性能网络框架,底层就是主从 Reactor + NIO。理解 Netty 的线程模型,就是理解 Reactor 模式的落地。
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 线程模型的关键要点:
accept 操作的频率远低于读写操作。一个端口上每秒能 accept 的连接数有限(受 TCP 三次握手和 backlog 队列限制),1 个线程绑死一个端口完全够用。如果有多个端口监听,可以配置多个 Boss 线程。
每个 NioEventLoop 线程绑定一个 Selector,负责一批 Channel 的 IO 操作。CPU*2 是一个经验值——在保证充分利用 CPU 的同时,避免过多的上下文切换。对于 IO 密集型场景可以适当增大。
/*
* 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());
1. 零拷贝:CompositeByteBuf 逻辑组合多个 Buffer,避免内存拷贝
2. 无锁串行化:同一 Channel 的所有操作在同一 EventLoop 线程执行,无需加锁
3. 内存池:ByteBuf 对象池化复用,减少 GC 压力
4. 直接内存:DirectByteBuffer 避免 JVM 堆与系统内存之间的拷贝
AIO / NIO.2:异步 IO 的理想与现实
JDK 7 引入了 NIO.2(也叫 AIO),提供真正的异步 IO:发起读写操作后立即返回,操作系统完成后通过回调通知。与 NIO 的"非阻塞 + 轮询事件"不同,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(); // 主线程阻塞等待
}
}
| 特性 | BIO | NIO | AIO (NIO.2) |
|---|---|---|---|
| IO 模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
| 编程模型 | 每连接一线程 | 事件驱动 + 回调 | Future / CompletionHandler |
| 线程模型 | 线程池 per connection | Reactor(少量线程) | OS 线程池回调 |
| 适用场景 | 连接少、短连接 | 大量长连接(通用) | 大量长连接 + 重 IO |
| Linux 支持 | 良好 | epoll 优秀 | io_uring(内核 5.1+) |
| 生态成熟度 | 高 | 高(Netty) | 低(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 原生还不支持。
总结:IO 模型全景与面试高频问题
| 维度 | BIO | NIO | AIO |
|---|---|---|---|
| 同步/异步 | 同步 | 同步 | 异步 |
| 阻塞/非阻塞 | 阻塞 | 非阻塞 | 非阻塞 |
| 编程复杂度 | 低 | 高 | 中 |
| 并发能力 | 低(千级) | 高(十万级) | 高(理论百万级) |
| JDK 版本 | 1.0 | 1.4 | 7 |
| 代表框架 | 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。"