1862 字
9 分钟
BIO 与 NIO 介绍对比、代码实现到异步通信架构扩展

本文按一个完整路径展开:

  1. 从 BIO 与 NIO 的核心对比开始。
  2. 通过代码示例看具体实现。
  3. 用 HttpClient async 看工程应用。
  4. 最后扩展到 MQ 双向队列实现提交响应的异步网络通信。

一、BIO(阻塞 IO) vs NIO(非阻塞 + 事件驱动)#

1. CPU 和线程阻塞的关系#

当一个线程在 BIO 上等待时:

  • 这个线程不会占用 CPU 执行指令。
  • CPU 可以调度其他线程执行任务。

所以从 CPU 利用率看,BIO 并不会天然浪费 CPU。

但当请求量很大、每个请求都需要一个线程时,问题会集中在:

  • 线程数量成千上万,线程创建开销大。
  • 线程切换频繁,上下文切换成本高。
  • 内存占用高,每个线程栈默认几百 KB 到 1 MB,线程过多容易 OOM。

结论:BIO 的性能瓶颈通常不是 CPU,而是线程管理开销。

2. NIO/事件驱动的优势#

NIO 使用少量线程 + 事件循环来管理大量连接。核心优势:

  • 减少线程数量,节省线程栈内存。
  • 减少上下文切换,CPU 不需要频繁保存/恢复线程状态。
  • 复用线程处理 IO 事件,单线程可处理成百上千连接。

可以理解为:NIO 提升的是系统吞吐量和资源效率,而不是单线程 CPU 计算性能。

3. 阶段总结#

  • NIO 优势主要在高并发、IO 密集场景。
  • NIO 代价是开发复杂度增加、调试难度提高,对 CPU 密集型任务帮助有限。
  • BIO 优势是简单易用、逻辑清晰,更适合低并发。

二、代码示例:怎么实现#

1. BIO 示例(传统阻塞)#

特点:一个连接占一个线程,不读完不撒手。

import java.io.*;
import java.net.*;
public class BioHttpServer {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8080);
System.out.println("BIO Server 启动 :8080");
while (true) {
// 阻塞在这里,等连接
Socket socket = ss.accept();
System.out.println("新连接: " + socket);
// 每个连接开一个线程
new Thread(() -> {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
OutputStream out = socket.getOutputStream()) {
// 阻塞读请求
String line;
while ((line = in.readLine()) != null && !line.isBlank()) {
System.out.println(line);
}
// 返回 HTTP 响应
String resp = "HTTP/1.1 200 OK\r\nContent-Length:11\r\n\r\nHello BIO!";
out.write(resp.getBytes());
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}

2. BIO 代码逐句解释(简单版)#

ServerSocket ss = new ServerSocket(8080);
  • 开一个服务器,监听 8080 端口。
while (true) {
Socket socket = ss.accept();
}
  • accept() 是阻塞的。
  • 没有连接进来,程序就卡在这里不动。
new Thread(() -> {
// 处理请求
}).start();
  • 每来一个客户端,新开一个线程专门处理。
  • 线程内部:
in.readLine() // 阻塞读,没数据就等
  • 读数据时也是阻塞,客户端不发数据,线程就会等待。
String resp = "HTTP/1.1 200 OK ... Hello BIO!";
out.write(resp.getBytes());
  • 组装 HTTP 响应并返回给客户端。

3. NIO 示例(非阻塞 + 事件驱动)#

特点:单线程循环多路复用,不阻塞,有事件才处理。

import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.Iterator;
public class NioHttpServer {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8081));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Server 启动 :8081");
while (true) {
// 阻塞等待事件(连接/读/写)
selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
// 新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel sc = server.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 可读事件
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
sc.read(buf);
// 简单 HTTP 响应
String resp = "HTTP/1.1 200 OK\r\nContent-Length:11\r\n\r\nHello NIO!";
ByteBuffer outBuf = ByteBuffer.wrap(resp.getBytes());
sc.write(outBuf);
sc.close();
}
}
}
}
}

4. NIO 代码逐句解释(重点)#

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 关键:非阻塞
ssc.bind(new InetSocketAddress(8081));
  • 打开 NIO 通道,设置为非阻塞。
  • 没有连接时,accept() 不会卡住,会直接返回 null
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
  • Selector 是多路复用器。
  • 服务器通道注册到 Selector,监听连接事件。
while (true) {
selector.select(); // 阻塞等事件(连接/读/写)
}
  • 没有事件时线程等待。
  • 有事件(连接、读写)时立即唤醒处理。
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
  • 拿到已发生事件的集合并遍历。

处理连接事件:

if (key.isAcceptable()) {
SocketChannel sc = server.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
  • 有客户端连入。
  • 客户端通道设置为非阻塞,并注册读事件。

处理读事件:

if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
sc.read(buf); // 非阻塞读,有多少读多少
}
  • 客户端发来数据后读取。
  • 读取过程不需要独占一个阻塞线程。
String resp = "HTTP/1.1 200 OK ... Hello NIO!";
ByteBuffer outBuf = ByteBuffer.wrap(resp.getBytes());
sc.write(outBuf);
sc.close();
  • 返回响应并关闭连接。

5. 最直白对比(一句话)#

  • BIO:来一个客人,开一个服务员,全程等着,啥也不干。
  • NIO:一个服务员盯着一堆客人,谁举手(有事件)就服务谁。

三、应用场景:Java HttpClient 示例#

  • client.send(...)同步阻塞
  • client.sendAsync(...)异步非阻塞(事件驱动)

0. 先导入包#

import java.net.*;
import java.net.http.*;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;

1. 同步请求(阻塞 BIO 风格)#

发请求后一直等响应回来,当前线程会阻塞。

public static void syncGet() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
try {
// 同步发送:这里会阻塞
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("状态码:" + response.statusCode());
System.out.println("响应体:" + response.body());
} catch (Exception e) {
e.printStackTrace();
}
}

2. 异步请求(非阻塞 NIO 风格)#

发请求后不等结果直接返回,响应回来自动回调。

public static void asyncGet() {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
// 异步发送,返回 CompletableFuture
CompletableFuture<HttpResponse<String>> future = client.sendAsync(
request,
HttpResponse.BodyHandlers.ofString()
);
// 回调:响应回来时执行
future.thenAccept(response -> {
System.out.println("异步状态码:" + response.statusCode());
System.out.println("异步响应:" + response.body());
});
// 异常处理
future.exceptionally(ex -> {
ex.printStackTrace();
return null;
});
// 防止主线程直接退出(测试用)
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
}
}

四、扩展:MQ 双向队列(提交 + 响应)#

  • 基于消息队列(RabbitMQ、Kafka、RocketMQ)。
  • 通过两个队列实现请求 + 响应:
    • 请求队列:客户端 -> 服务端。
    • 响应队列:服务端 -> 客户端。
  • 本质:中间件存储、解耦、异步可靠通信、非实时强依赖。

2. 与HTTP 异步请求的区别#

维度HTTP 异步请求MQ 双向队列(提交+响应)
通信模型点对点直连,请求-响应基于中间件,发布-订阅/点对点
是否依赖中间件无,直接端到端必须依赖 MQ 服务
消息是否落地不落地,传输失败即丢失持久化存储,丢包率极低
耦合度强耦合:必须对方在线才能发弱耦合:一方离线不影响发送
流量削峰不支持,直接压到目标服务天然支持,缓冲洪峰流量
超时/重试依赖 HTTP 超时,手动处理内置重试、死信、确认机制
响应实时性低延迟,实时性高略高延迟,最终一致性
传输可靠性一般,网络波动易失败极高,消息可保证送达
适用场景实时接口调用、同步转异步解耦、削峰、可靠异步通信
  • HTTP 异步:只是调用方线程不阻塞,通信仍是实时直连。
  • MQ 双向队列:通过中间件存储并转发消息,实现解耦与可靠异步。

总结#

  1. HTTP async:轻量、实时、直连、无中间件,适合实时接口异步调用。
  2. MQ 双向队列:可靠、解耦、削峰、可持久化,适合高可靠、高吞吐、弱实时的异步提交响应。
  3. 两者虽然都叫异步,但一个是直连异步,一个是中间件异步,解决的问题不同。
BIO 与 NIO 介绍对比、代码实现到异步通信架构扩展
https://hyglgithub.github.io/AstroBlog/posts/20260317/
作者
Wok
发布于
2026-03-17
许可协议
CC BY-NC-SA 4.0