1862 字
9 分钟
BIO 与 NIO 介绍对比、代码实现到异步通信架构扩展
本文按一个完整路径展开:
- 从 BIO 与 NIO 的核心对比开始。
- 通过代码示例看具体实现。
- 用 HttpClient async 看工程应用。
- 最后扩展到 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 双向队列:通过中间件存储并转发消息,实现解耦与可靠异步。
总结
- HTTP async:轻量、实时、直连、无中间件,适合实时接口异步调用。
- MQ 双向队列:可靠、解耦、削峰、可持久化,适合高可靠、高吞吐、弱实时的异步提交响应。
- 两者虽然都叫异步,但一个是直连异步,一个是中间件异步,解决的问题不同。
BIO 与 NIO 介绍对比、代码实现到异步通信架构扩展
https://hyglgithub.github.io/AstroBlog/posts/20260317/