编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
        • 11.1 开篇疑问
        • 11.2 理解IO的本质
          • 11.2.1 用户态与内核态
          • 11.2.2 IO操作的两个阶段
          • 11.2.3 同步异步与阻塞非阻塞
        • 11.3 五种IO模型
          • 11.3.1 阻塞IO模型
          • 11.3.2 非阻塞IO模型
          • 11.3.3 IO多路复用模型
          • 11.3.4 信号驱动IO模型
          • 11.3.5 异步IO模型
          • 11.3.6 五种模型对比总结
        • 11.4 Java BIO详解
          • 11.4.1 BIO编程模型
          • 11.4.2 BIO的瓶颈
          • 11.4.3 伪异步IO优化
        • 11.5 Java NIO核心原理
          • 11.5.1 三大核心组件
          • 11.5.2 Channel详解
          • 11.5.3 Buffer深度设计
          • 11.5.4 DirectBuffer与堆外内存
          • 11.5.5 Selector多路复用
          • 11.5.6 select/poll/epoll底层对比
          • 11.5.7 epoll的工作原理
          • 11.5.8 零拷贝原理
          • 11.5.9 内存映射mmap
        • 11.6 Java AIO异步IO
        • 11.7 Reactor模式与Netty
          • 11.7.1 Reactor模式的三种变体
          • 11.7.2 Netty的线程模型
          • 11.7.3 Netty为什么选择NIO
        • 11.8 常见面试深度问题
        • 11.9 总结与核心要点
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

IO模型演进BIO到AIO

# 40.IO模型演进BIO到AIO

# 目录介绍

  • 11.1 开篇疑问
  • 11.2 理解IO的本质
    • 11.2.1 用户态与内核态
    • 11.2.2 IO操作的两个阶段
    • 11.2.3 同步异步与阻塞非阻塞
  • 11.3 五种IO模型
    • 11.3.1 阻塞IO模型
    • 11.3.2 非阻塞IO模型
    • 11.3.3 IO多路复用模型
    • 11.3.4 信号驱动IO模型
    • 11.3.5 异步IO模型
    • 11.3.6 五种模型对比总结
  • 11.4 Java BIO详解
    • 11.4.1 BIO编程模型
    • 11.4.2 BIO的瓶颈
    • 11.4.3 伪异步IO优化
  • 11.5 Java NIO核心原理
    • 11.5.1 三大核心组件
    • 11.5.2 Channel详解
    • 11.5.3 Buffer深度设计
    • 11.5.4 DirectBuffer与堆外内存
    • 11.5.5 Selector多路复用
    • 11.5.6 select/poll/epoll底层对比
    • 11.5.7 epoll的工作原理
    • 11.5.8 零拷贝原理
    • 11.5.9 内存映射mmap
  • 11.6 Java AIO异步IO
  • 11.7 Reactor模式与Netty
    • 11.7.1 Reactor模式的三种变体
    • 11.7.2 Netty的线程模型
    • 11.7.3 Netty为什么选择NIO
  • 11.8 常见面试深度问题
  • 11.9 总结与核心要点

# 11.1 开篇疑问

疑惑:Java 的 IO 为什么有 BIO、NIO、AIO 三代?阻塞和非阻塞有什么区别?同步和异步又是什么意思?Netty 这种高性能框架底层用的是哪种 IO?epoll 为什么比 select 快?

答疑:IO 模型的演进直接影响了服务端的并发处理能力。从 BIO 的"一连接一线程"到 NIO 的"一线程管多连接",再到 AIO 的"完全异步",每一步都是对系统资源利用率的深度优化。

# 11.2 理解IO的本质

# 11.2.1 用户态与内核态

应用程序不能直接操作硬件(网卡、磁盘),必须通过操作系统内核。IO 操作就是应用程序向内核发起请求,内核与硬件交互后将数据返回给应用。

应用程序(用户态)
    │ 系统调用 (read/write/accept/connect)
    ↓
操作系统内核(内核态)
    │ 驱动程序
    ↓
硬件(网卡/磁盘)
1
2
3
4
5
6
7

系统调用的开销:每次系统调用都涉及用户态→内核态的上下文切换(保存/恢复寄存器、切换内存空间等),大约几微秒。频繁的系统调用会严重影响性能。

# 11.2.2 IO操作的两个阶段

一次网络 IO 读操作分为两个阶段:

  1. 等待数据就绪:数据从网卡到达内核缓冲区(数据从网络传输到内核)
  2. 数据拷贝:数据从内核缓冲区拷贝到用户缓冲区(内核空间→用户空间)
网卡 ──→ 内核缓冲区 ──→ 用户缓冲区
          阶段1              阶段2
       (等待数据)         (拷贝数据)
1
2
3

不同 IO 模型的区别,就在于这两个阶段是否阻塞。

# 11.2.3 同步异步与阻塞非阻塞

这两组概念经常被混淆,但它们描述的是不同维度:

阻塞/非阻塞:描述调用者的行为
  阻塞:线程被挂起,等待操作完成后才返回
  非阻塞:调用立即返回,不等待操作完成

同步/异步:描述操作的完成方式
  同步:调用者自己等待结果(主动检查)
  异步:操作完成后通过回调/通知告知调用者(被动接收)

类比:
  去餐厅点菜
  - 同步阻塞(BIO):在柜台等,一直站着等菜做好
  - 同步非阻塞(NIO):点完菜后逛一圈,每隔几分钟来柜台问一次
  - 异步非阻塞(AIO):点完菜后逛街,菜做好了服务员打电话通知你
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11.3 五种IO模型

# 11.3.1 阻塞IO模型

应用线程                    内核
  │                         │
  │── read() ──→            │
  │  (阻塞等待)              │ ← 等待数据到达(阶段1)
  │  (阻塞等待)              │ ← 数据从内核拷贝到用户(阶段2)
  │←── 返回数据 ──          │
  │                         │
1
2
3
4
5
6
7

两个阶段都阻塞。线程在等待 IO 完成期间无法做其他事。

# 11.3.2 非阻塞IO模型

应用线程                    内核
  │── read() ──→            │
  │←── EAGAIN ──            │ 数据没好
  │── read() ──→            │
  │←── EAGAIN ──            │ 数据还没好
  │── read() ──→            │
  │  (阻塞)                 │ ← 数据拷贝(阶段2仍阻塞)
  │←── 返回数据 ──          │
1
2
3
4
5
6
7
8

等待阶段不阻塞(立即返回),但需要轮询。数据拷贝阶段仍然阻塞。CPU 空转浪费严重。

# 11.3.3 IO多路复用模型

应用线程                    内核
  │── select(fds) ──→      │
  │  (阻塞在select上)       │ ← 同时监听多个fd
  │←── fd就绪通知 ──        │ ← 某个或某些fd就绪
  │── read(fd) ──→          │
  │  (阻塞)                 │ ← 数据拷贝
  │←── 返回数据 ──          │
1
2
3
4
5
6
7

核心思想:用一个线程(一次系统调用)监控多个 IO 通道,哪个就绪就处理哪个。这是 Java NIO 和 Netty 的基础。

实现 特点 最大fd数
select 每次调用全量拷贝fd集合,全量遍历 1024 (FD_SETSIZE)
poll 用链表存储fd,无数量限制,但仍全量遍历 无限制
epoll 事件驱动,只返回就绪的fd,性能最优 无限制(受内存限制)

# 11.3.4 信号驱动IO模型

应用线程                    内核
  │── sigaction(SIGIO) ──→ │  注册信号处理函数
  │  (继续执行其他操作)      │
  │                         │ ← 数据到达
  │←── SIGIO 信号 ──       │  内核发送信号
  │── read() ──→            │
  │  (阻塞)                 │ ← 数据拷贝
  │←── 返回数据 ──          │
1
2
3
4
5
6
7
8

数据就绪时内核发送 SIGIO 信号通知应用。阶段1不阻塞,阶段2阻塞。UDP 场景可用,TCP 场景不实用(信号过多)。

# 11.3.5 异步IO模型

应用线程                    内核
  │── aio_read() ──→       │
  │  (立即返回,继续执行)    │ ← 内核完成等待+拷贝
  │  (做其他事)              │    (两个阶段都由内核处理)
  │←── 回调通知 ──          │ 全部完成
1
2
3
4
5

两个阶段都不阻塞。内核完成所有工作后通知应用。这是真正的异步。

# 11.3.6 五种模型对比总结

                    阶段1(等待数据)    阶段2(拷贝数据)
阻塞IO              阻塞              阻塞
非阻塞IO            非阻塞(轮询)       阻塞
IO多路复用           阻塞(在select上)  阻塞
信号驱动IO           非阻塞(信号通知)   阻塞
异步IO              非阻塞            非阻塞
                    ↑                ↑
                    前四种都是同步IO    只有AIO是异步IO
1
2
3
4
5
6
7
8

关键认知:IO 多路复用(select/epoll)本质上还是同步IO,因为数据拷贝阶段仍需要用户线程参与。只有 AIO 是真正的异步——内核完成所有工作后通知应用。

# 11.4 Java BIO详解

# 11.4.1 BIO编程模型

// BIO 服务端:一连接一线程
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket socket = serverSocket.accept();  // 阻塞等待连接
    new Thread(() -> {
        try {
            InputStream in = socket.getInputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {  // 阻塞等待数据
                String msg = new String(buffer, 0, len);
                System.out.println("收到: " + msg);
                
                // 响应
                OutputStream out = socket.getOutputStream();
                out.write(("Echo: " + msg).getBytes());
                out.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 11.4.2 BIO的瓶颈

每个连接需要一个独立线程处理。当连接数到达数千时:

  • 线程数暴增(每个线程占 1MB 栈内存)
  • 大量线程上下文切换(每次切换约 1-10μs)
  • 大部分线程在阻塞等待,CPU 利用率极低
1000 连接 → 1000 线程 → ~1GB 栈内存 + 大量上下文切换
10000 连接 → 10000 线程 → 系统崩溃
C10K 问题:如何同时处理10000个并发连接?
1
2
3

# 11.4.3 伪异步IO优化

用线程池限制线程数,但根本问题未解决:

// 伪异步 IO:用线程池处理连接
ExecutorService pool = Executors.newFixedThreadPool(200);
ServerSocket serverSocket = new ServerSocket(8080);

while (true) {
    Socket socket = serverSocket.accept();
    pool.execute(() -> {
        // 处理连接...
        // 如果客户端不发数据,线程仍然阻塞在 read() 上
        // 200个线程池如果全部阻塞,新连接将被拒绝
    });
}

// 问题:200个线程全部阻塞在 read(),
// 即使有新连接进来,也没有线程可用
// 本质:阻塞模型下,一个线程只能服务一个连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 11.5 Java NIO核心原理

# 11.5.1 三大核心组件

组件 作用 类比
Channel 双向数据通道 水管(可读可写)
Buffer 数据缓冲区 水桶(临时存储)
Selector 多路复用器 调度员(管理多个通道)
                Selector(一个线程)
               /    |    \
        Channel1  Channel2  Channel3
           |         |         |
        Buffer    Buffer     Buffer
1
2
3
4
5

与 BIO 的关键区别:BIO 面向流(Stream),NIO 面向缓冲区(Buffer)。BIO 的 InputStream/OutputStream 是单向的,NIO 的 Channel 是双向的。

# 11.5.2 Channel详解

// Channel 的常见实现
FileChannel        // 文件读写(不能设置非阻塞)
SocketChannel      // TCP 客户端
ServerSocketChannel // TCP 服务端
DatagramChannel    // UDP

// Channel 的双向性
FileChannel channel = FileChannel.open(path, READ, WRITE);
channel.read(buffer);   // 从 Channel 读到 Buffer
channel.write(buffer);  // 从 Buffer 写到 Channel

// SocketChannel 的非阻塞模式
SocketChannel client = SocketChannel.open();
client.configureBlocking(false);  // 设置非阻塞
client.connect(new InetSocketAddress("localhost", 8080));

// 非阻塞模式下,connect/read/write 都不会阻塞
// connect() 可能立即返回 false(连接还未完成)
// read() 可能返回 0(没有数据可读)
while (!client.finishConnect()) {
    // 等待连接完成
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 11.5.3 Buffer深度设计

Buffer 的核心是四个属性:

┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ H │ e │ l │ l │ o │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
  ↑                   ↑                   ↑
 mark              position            capacity
                   limit=10

0 <= mark <= position <= limit <= capacity
1
2
3
4
5
6
7
8
属性 含义
capacity 缓冲区总容量,创建后不变
limit 可读/写的边界
position 当前读/写位置
mark 标记位,调用 reset() 可以回到 mark 的位置

关键操作:

ByteBuffer buffer = ByteBuffer.allocate(10);

// 写模式(初始状态)
// position=0, limit=10, capacity=10
buffer.put("Hello".getBytes());
// position=5, limit=10, capacity=10

// flip() 切换到读模式
buffer.flip();
// position=0, limit=5, capacity=10
// flip 做了两件事:limit=position, position=0

// 读取数据
byte b = buffer.get();  // position++
// position=1, limit=5

// compact() 压缩:把未读数据移到开头,继续写
buffer.compact();
// 将 position~limit 的数据拷贝到 0~(limit-position)
// position = limit-position, limit = capacity

// clear() 清空(只重置指针,不清数据)
buffer.clear();
// position=0, limit=capacity

// rewind() 重新读:position 回到 0,limit 不变
buffer.rewind();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Buffer 使用的标准模板:

ByteBuffer buffer = ByteBuffer.allocate(1024);

// 从 Channel 读数据到 Buffer
int bytesRead = channel.read(buffer);  // 写模式

// 切换到读模式
buffer.flip();

// 读取 Buffer 中的数据
while (buffer.hasRemaining()) {
    byte b = buffer.get();
}

// 清空 Buffer,准备下一次写入
buffer.clear();  // 或 buffer.compact()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 11.5.4 DirectBuffer与堆外内存

// HeapByteBuffer:在 JVM 堆中分配
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// DirectByteBuffer:在堆外(本地内存)分配
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
1
2
3
4
5
特性 HeapByteBuffer DirectByteBuffer
内存位置 JVM 堆 本地内存(堆外)
分配速度 快 慢(需要系统调用)
GC 影响 受 GC 管理 不受 GC 直接管理
IO 效率 需要额外拷贝 零拷贝(内核直接访问)
适用场景 临时、小缓冲区 长期使用、大缓冲区

为什么 DirectBuffer IO 更快?

HeapBuffer 的 IO 过程(多一次拷贝):
  用户空间 HeapBuffer → 临时 DirectBuffer → 内核缓冲区

DirectBuffer 的 IO 过程(少一次拷贝):
  用户空间 DirectBuffer → 内核缓冲区

原因:JVM 堆内存可能被 GC 移动(地址变化)
内核需要稳定的内存地址做 DMA 传输
所以 HeapBuffer 需要先拷贝到堆外的固定地址区域
1
2
3
4
5
6
7
8
9

# 11.5.5 Selector多路复用

一个 Selector 可以管理成千上万个 Channel,实现"一个线程处理多个连接":

// NIO 服务端核心模型
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    int readyCount = selector.select();  // 阻塞直到有事件就绪
    if (readyCount == 0) continue;
    
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> iter = keys.iterator();
    
    while (iter.hasNext()) {
        SelectionKey key = iter.next();
        
        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();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len = client.read(buffer);
            if (len > 0) {
                buffer.flip();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                System.out.println("收到: " + new String(data));
            } else if (len == -1) {
                key.cancel();
                client.close();
            }
        }
        iter.remove();  // 必须移除已处理的 key!
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

SelectionKey 的四种事件:

事件 常量 触发条件
OP_ACCEPT 16 ServerSocketChannel 收到新连接
OP_CONNECT 8 SocketChannel 连接完成
OP_READ 1 Channel 有数据可读
OP_WRITE 4 Channel 可以写数据

# 11.5.6 select/poll/epoll底层对比

特性 select poll epoll
数据结构 bitmap (fd_set) 数组 (pollfd) 红黑树 + 就绪链表
fd 数量限制 1024 无限制 无限制
传递方式 每次拷贝全量fd 每次拷贝全量fd fd 只需注册一次
检查方式 线性遍历所有fd 线性遍历所有fd 回调,只返回就绪fd
时间复杂度 O(n) O(n) O(1)(就绪事件)
触发方式 水平触发 水平触发 水平触发+边缘触发

# 11.5.7 epoll的工作原理

// epoll 三个核心系统调用

// 1. epoll_create: 创建 epoll 实例(红黑树 + 就绪链表)
int epfd = epoll_create(1);

// 2. epoll_ctl: 注册/修改/删除 fd 的监听事件
//    fd 插入红黑树,同时注册回调函数
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);

// 3. epoll_wait: 等待就绪事件
//    检查就绪链表,如果为空则阻塞
//    有 fd 就绪时,回调函数将 fd 加入就绪链表
int n = epoll_wait(epfd, events, maxevents, timeout);
1
2
3
4
5
6
7
8
9
10
11
12
13

epoll 的内部数据结构:

epoll 实例
├── 红黑树(存储所有注册的 fd)
│   ├── fd1 + callback
│   ├── fd2 + callback
│   └── fd3 + callback
└── 就绪链表(存储有事件的 fd)
    ├── fd1(有数据可读)
    └── fd3(有新连接)

当 fd 上有事件时:
  → 网卡中断 → 内核处理 → 执行 fd 的回调函数
  → 回调将 fd 加入就绪链表
  → epoll_wait 返回就绪链表中的 fd
1
2
3
4
5
6
7
8
9
10
11
12
13

水平触发(LT) vs 边缘触发(ET):

水平触发(Level Triggered,默认):
  fd 就绪后,如果没有处理完,下次 epoll_wait 还会返回
  类似:水位线,只要水位高于阈值就通知
  安全但可能频繁唤醒

边缘触发(Edge Triggered):
  fd 状态变化时才通知,只通知一次
  类似:水位变化的瞬间才通知
  高效但必须一次性处理完所有数据(否则丢失事件)
  Netty 默认使用 ET 模式
1
2
3
4
5
6
7
8
9
10

# 11.5.8 零拷贝原理

传统 IO 需要 4 次数据拷贝和 4 次上下文切换:

传统IO(read + write):
  1. read(): 用户态→内核态(上下文切换)
  2. DMA 拷贝: 磁盘 → 内核缓冲区(Page Cache)
  3. CPU 拷贝: 内核缓冲区 → 用户缓冲区
  4. read() 返回: 内核态→用户态(上下文切换)
  5. write(): 用户态→内核态(上下文切换)
  6. CPU 拷贝: 用户缓冲区 → Socket 缓冲区
  7. DMA 拷贝: Socket 缓冲区 → 网卡
  8. write() 返回: 内核态→用户态(上下文切换)

总计: 4次拷贝 + 4次上下文切换
1
2
3
4
5
6
7
8
9
10
11

sendfile 零拷贝:

sendfile() 零拷贝:
  1. sendfile(): 用户态→内核态(上下文切换)
  2. DMA 拷贝: 磁盘 → 内核缓冲区
  3. CPU 拷贝: 内核缓冲区 → Socket 缓冲区(Linux 2.4+ 可省略)
  4. DMA 拷贝: Socket 缓冲区 → 网卡
  5. sendfile() 返回: 内核态→用户态(上下文切换)

总计: 2-3次拷贝 + 2次上下文切换

Linux 2.4+ (DMA Scatter/Gather):
  只需 2 次 DMA 拷贝,0 次 CPU 拷贝
  内核缓冲区的描述符直接传给网卡
1
2
3
4
5
6
7
8
9
10
11
12
// Java NIO 的零拷贝实现
FileChannel fileChannel = FileChannel.open(path);

// transferTo 底层调用 sendfile
fileChannel.transferTo(0, fileChannel.size(), socketChannel);

// Kafka、Netty 的文件传输都使用了零拷贝
1
2
3
4
5
6
7

# 11.5.9 内存映射mmap

// mmap 将文件直接映射到用户空间内存
// 省去了内核缓冲区→用户缓冲区的拷贝
FileChannel channel = FileChannel.open(path, READ, WRITE);
MappedByteBuffer mmap = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());

// 直接操作映射的内存区域
mmap.put(0, (byte) 'H');  // 修改会反映到文件
byte b = mmap.get(0);     // 读取文件内容

// mmap 的优势:
// 1. 减少数据拷贝(内核空间和用户空间共享同一块物理内存)
// 2. 文件读写变成内存操作(PageFault 由内核自动处理)
// 3. RocketMQ 的 CommitLog 使用 mmap 提升性能
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11.6 Java AIO异步IO

Java 7 引入 AIO(NIO.2),真正的异步非阻塞:

AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));

// 异步接受连接——注册回调
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
    @Override
    public void completed(AsynchronousSocketChannel client, Void att) {
        server.accept(null, this);  // 继续接受下一个连接
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer buf) {
                buf.flip();
                byte[] data = new byte[buf.remaining()];
                buf.get(data);
                System.out.println("收到: " + new String(data));
            }
            @Override
            public void failed(Throwable exc, ByteBuffer buf) {
                exc.printStackTrace();
            }
        });
    }
    @Override
    public void failed(Throwable exc, Void att) {
        exc.printStackTrace();
    }
});

// 也支持 Future 方式
Future<AsynchronousSocketChannel> future = server.accept();
AsynchronousSocketChannel client = future.get();  // 阻塞等待
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

AIO 的现状:AIO 在 Linux 上的实现底层仍然基于 epoll(模拟异步),并非真正的内核异步IO(io_uring 是 Linux 5.1 才引入的真正异步IO)。因此 AIO 在 Linux 上性能提升有限,Netty 等框架并未采用。

各平台 AIO 实现:
Linux:   基于 epoll 模拟(非真正异步) → 性能无优势
Windows: 基于 IOCP(真正异步)        → 但 Java 服务端很少用 Windows
macOS:   基于 kqueue                 → 支持不完善
1
2
3
4

# 11.7 Reactor模式与Netty

# 11.7.1 Reactor模式的三种变体

1. 单Reactor单线程(Redis 6.0前):

         ┌──────────────────────────────┐
Clients →│ Reactor(select + dispatch)  │
         │   ↓         ↓         ↓      │
         │ accept    read/send  decode  │
         │ (全部在一个线程中处理)          │
         └──────────────────────────────┘
1
2
3
4
5
6

2. 单Reactor多线程:

         ┌──────────────┐
Clients →│ Reactor       │ ← select + dispatch
         │ (主线程)      │
         └──────┬───────┘
                │
         ┌──────▼───────┐
         │ Worker Pool   │ ← 多个工作线程处理业务
         │ Thread 1-N    │
         └──────────────┘
1
2
3
4
5
6
7
8
9

3. 主从Reactor多线程(Netty 默认):

         ┌──────────────┐
Clients →│ Main Reactor  │ ← 只负责 accept
         │ (Boss Group)  │
         └──────┬───────┘
                │ 将新连接分配给
         ┌──────▼───────┐
         │ Sub Reactors  │ ← 每个 Sub Reactor 管理一组连接的 IO
         │ (Worker Group)│    每个 Sub Reactor 一个线程 + 一个 Selector
         │ Reactor 1     │
         │ Reactor 2     │
         │ ...           │
         └──────┬───────┘
                │ 业务处理交给
         ┌──────▼───────┐
         │ Business Pool │ ← 可选的业务线程池
         └──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 11.7.2 Netty的线程模型

// Netty 的主从 Reactor 模型
EventLoopGroup bossGroup = new NioEventLoopGroup(1);    // Main Reactor
EventLoopGroup workerGroup = new NioEventLoopGroup();   // Sub Reactors

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) {
            ch.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast(new MyBusinessHandler());
        }
    });

bootstrap.bind(8080).sync();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Netty 的核心设计:

EventLoop = 一个线程 + 一个 Selector + 一个任务队列
  → 一个 EventLoop 管理多个 Channel
  → 一个 Channel 只绑定一个 EventLoop(线程安全!)
  → 所有 IO 操作都在 EventLoop 线程中执行

EventLoopGroup = 多个 EventLoop
  → BossGroup: 通常1个 EventLoop,负责 accept
  → WorkerGroup: 通常 CPU核心数×2 个 EventLoop,负责 IO
1
2
3
4
5
6
7
8

# 11.7.3 Netty为什么选择NIO

考量 NIO AIO
Linux 支持 epoll 成熟高效 基于 epoll 模拟,无真正优势
Windows 不太关注 IOCP 真异步,但 Java 服务端少用 Windows
编程模型 Reactor 模式,清晰可控 回调嵌套,代码复杂(回调地狱)
社区生态 成熟稳定,大量生产验证 使用较少
连接数 大量连接少量活跃时优势明显 理论上所有场景都优

Netty 基于 NIO + Reactor 模式,结合 EventLoop 实现了高性能的网络通信框架。Dubbo、gRPC、Elasticsearch、Kafka 等都使用 Netty。

# 11.8 常见面试深度问题

Q1:NIO 的 Selector 空轮询 bug 是什么?

// JDK 的 epoll 实现有一个 bug(JDK-6670302)
// 在某些情况下,select() 本应阻塞,但立即返回 0
// 导致 while(true) 死循环,CPU 100%

// Netty 的解决方案:
// 记录 select 空轮询的次数
// 当空轮询次数超过阈值(默认512次),重建 Selector
if (selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
    rebuildSelector();  // 创建新的 Selector,重新注册所有 Channel
}
1
2
3
4
5
6
7
8
9
10

Q2:为什么 Netty 不直接使用 JDK 的 NIO?

  1. JDK NIO 的 API 复杂,容易出错(忘记 remove SelectionKey、Buffer flip 等)
  2. Selector 空轮询 bug
  3. 没有完善的异常处理和重连机制
  4. 没有拆包/粘包处理
  5. Netty 封装了更高效的 ByteBuf(池化、引用计数、零拷贝)

Q3:什么是粘包和拆包?

TCP 是面向字节流的协议,没有消息边界的概念

发送方发送两条消息: "Hello" + "World"
接收方可能收到:
  1. "HelloWorld"     → 粘包
  2. "Hel" + "loWorld" → 拆包
  3. "Hello" + "World" → 正常(运气好)

解决方案(Netty 内置支持):
  1. 固定长度: FixedLengthFrameDecoder
  2. 分隔符:   DelimiterBasedFrameDecoder
  3. 长度字段: LengthFieldBasedFrameDecoder(最常用)
  4. 自定义协议: 消息头(长度)+ 消息体
1
2
3
4
5
6
7
8
9
10
11
12
13

# 11.9 总结与核心要点

IO 模型演进:

模型 等待阶段 拷贝阶段 线程模型 典型应用
BIO 阻塞 阻塞 一连接一线程 传统 Servlet
NIO 非阻塞(多路复用) 阻塞 一线程管多连接 Netty、Tomcat NIO
AIO 非阻塞 非阻塞 回调/Future 少量使用

核心设计思想:

  1. 多路复用是关键突破:从"一连接一线程"变为"一线程管多连接",支撑了百万级并发
  2. 事件驱动:Selector/epoll 只通知有事件的 Channel,避免了无效遍历
  3. 零拷贝:减少数据在用户态和内核态之间的复制次数,提升传输效率
  4. Reactor 模式:主从 Reactor 分离连接接入和 IO 处理,充分利用多核

核心要点速查:

问题 答案
BIO 的瓶颈 一连接一线程,线程资源耗尽
NIO 的核心 Channel + Buffer + Selector
epoll 比 select 快 红黑树 + 就绪链表 + 回调,O(1) 获取就绪fd
零拷贝实现 sendfile(transferTo)、mmap
AIO 为何不流行 Linux 上基于 epoll 模拟,无真正优势
Netty 用什么模型 主从 Reactor + NIO(epoll)
Buffer 的 flip limit=position, position=0,切换读模式
DirectBuffer 优势 避免堆内存到堆外的拷贝,IO 更高效

理解 IO 模型,是理解高性能网络编程(Netty、Tomcat、Redis)的基础。

上次更新: 2026/06/10, 11:13:41
CompletableFuture异步
ByteBuffer与堆外内存

← CompletableFuture异步 ByteBuffer与堆外内存→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式