Java BIO / NIO / AIO 完整详解 + 编程技巧
- BIO:同步阻塞
- NIO:同步非阻塞(多路复用)
- AIO:异步非阻塞(NIO.2)
一、BIO 阻塞 IO
原理
BIO = 一连接一线程。accept()、read()、write()全是阻塞调用。 没有客户端连接,accept()卡住;连接建立后没有数据,read()卡住线程。
简易代码
public class BioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("BIO服务启动");
while (true) {
// 阻塞:没有连接就卡住
Socket socket = serverSocket.accept();
// 每个连接新开一个线程
new Thread(() -> {
try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("收到客户端:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
✅ BIO 编程技巧
-
不要直接无限制创建线程:生产要用线程池版本 BIO,限制最大连接,防止线程打爆 OS。
ExecutorService pool = Executors.newFixedThreadPool(10);
// 收到socket后 pool.submit(任务); -
必须关闭流和 Socket,使用 try-with-resources 自动释放资源,避免文件句柄泄漏。
-
BIO 适合短连接、并发量很小场景;连接数上百就会出现大量线程上下文切换、内存飙升。
-
问题:连接空闲的时候线程也被占用,线程资源浪费严重。
❌ BIO 痛点
- 连接越多,线程越多,操作系统线程上限有限。
- 线程阻塞,无法复用。
二、NIO(Java NIO,NIO.1)同步非阻塞,多路复用【重点】
三大核心组件
- Channel 通道 :双向读写,类似 Socket,
SocketChannel / ServerSocketChannel / FileChannel - Buffer 缓冲区 :数据读写必须经过 Buffer。
ByteBuffer最常用。 - Selector 选择器 :多路复用器,一个线程监听多个 Channel 事件。
事件类型:OP_ACCEPT(连接就绪)、OP_READ(读就绪)、OP_WRITE(写就绪)
关键:NIO 只是等待数据阶段非阻塞 ,数据从内核拷贝到用户缓冲区,仍然是应用线程自己完成(同步)
NIO 服务端最简示例
public class NioServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8888));
// 设置通道非阻塞!!NIO核心开关
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
// 注册ACCEPT事件到selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞,等待就绪事件(可以带超时 selector.select(1000))
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove(); // 必须手动移除,否则重复处理
if (key.isAcceptable()) {
// 接受连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = server.accept();
clientChannel.configureBlocking(false);
// 注册读事件
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} else if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
buf.clear();
int read = channel.read(buf);
if (read > 0) {
buf.flip(); // flip:切换读写模式!NIO高频易错点
byte[] data = new byte[buf.remaining()];
buf.get(data);
System.out.println("NIO收到:" + new String(data));
} else if (read == -1) {
// 客户端关闭连接
key.cancel();
channel.close();
}
}
}
}
}
}
✅ NIO 编程技巧(高频踩坑点)
configureBlocking(false)必须设置,否则仍然阻塞。flip()翻转缓冲区 :write模式:position 写,limit=capacity- flip:limit=position,position=0,切换到读模式。
clear()不是清空数据,只是重置 position/limit。
selectedKeys集合必须迭代时 remove,事件处理完不删除,会重复触发旧事件,死循环。- JDK1.6 及之前 Selector 存在空轮询 bug :
select()返回 0,CPU100%。Netty 已经做了修复。 - OP_WRITE 事件不要随便注册:写事件频繁就绪,会疯狂触发,一般只有缓冲区满的时候才注册 OP_WRITE。
- ByteBuffer 是固定容量,半包 / 粘包需要自己处理协议(Netty 封装)。
- 单 Selector 单线程模型适合 IO 密集;高并发可以用主从 Selector(Reactor):一个 selector 专门接受连接,多个 selector 处理读写(Netty 默认模型)。
❌ NIO 痛点
原生 NIO API 极其繁琐,半包粘包、异常处理、断线检测全部手写,生产一般直接用 Netty 封装。
三、AIO NIO.2(异步非阻塞 IO)
原理
发起 IO 请求后,线程立刻返回,操作系统内核完成【等待数据 + 内核拷贝到应用缓冲区】 ,全部做完后回调CompletionHandler。 Java 核心类:AsynchronousServerSocketChannel、AsynchronousSocketChannel
AIO 服务端简单示例
public class AioServer {
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(8888));
// 注册接受连接回调
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
// 继续接收下一个连接
server.accept(null, this);
ByteBuffer buf = ByteBuffer.allocate(1024);
// 发起异步读,读完自动回调
client.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("AIO收到:" + new String(data));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 主线程阻塞等待,防止程序退出
LockSupport.park();
}
}
✅ AIO 编程技巧
- AIO 依赖操作系统异步 IO 支持,Linux 下 AIO 底层实现弱,Windows IOCP 很强,所以 Java 服务端很少用。
- 回调是嵌套式,业务复杂后回调地狱,可读性差。
- 回调线程由系统线程池提供,注意线程安全。
- 连接关闭、异常要捕获,否则容易句柄泄漏。
❌ AIO 痛点
Linux 下对网络 AIO 支持不完善,Netty5 曾经尝试 AIO,后来废弃。业务开发基本不用 AIO。
四、三大模型对比表
表格
| 模型 | IO 类型 | 线程模型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| BIO | 同步阻塞 | 1 连接 1 线程 | 代码最简单 | 连接多线程爆炸 | 低并发、少量连接 |
| NIO | 同步非阻塞,多路复用 | 少量线程管理大量连接 | 连接数高,资源省 | API 复杂,半包粘包需要自己处理 | 高并发长连接:IM、网关、RPC(Netty) |
| AIO | 异步非阻塞 | 内核回调 | 线程利用率最高 | Linux 支持差,回调嵌套复杂 | Windows 服务器,Java 后端极少使用 |