1. 操作系统 & 网络 IO 模型

IO的本质

IO = 数据在用户态 ↔ 内核态之间的拷贝,普通程序运行在用户态,而硬件资源由内核态统一管理,应用程序不能直接访问硬件,必须通过内核。

IO的两个阶段:

等待阶段(Waiting):数据是否从网卡到达内核缓冲区

拷贝阶段(Copying):内核缓冲区 → 用户缓冲区

在等待阶段可能发生阻塞/非阻塞,而在拷贝阶段可能发生同步/异步

阻塞/非阻塞:

阻塞 IO(Blocking IO)

阻塞 IO 在等待数据期间会让线程挂起

特点:调用 read() 后:若数据没准备好则线程阻塞,若数据准备好则继续执行。

编程简单,但线程资源浪费

非阻塞 IO(Non-blocking IO)

非阻塞 IO 解决了线程阻塞问题,但带来了 CPU 浪费

特点:调用read() 立即返回,若数据没好则返回 -1 / 异常,程序需要不断轮询

会导致CPU 空转

同步/异步:

同步 IO(Synchronous IO):

数据拷贝由应用线程发起并等待完成,即使是非阻塞 IO,也属于同步 IO

异步 IO(Asynchronous IO):

应用只发起请求,内核完成数据拷贝,通过回调 / 信号通知应用,应用无需等待

五种IO模型

阻塞IO(BIO)

工作流程:

read()调用→等待数据→拷贝数据→返回结果

特点:简单,并发能力差

非阻塞IO

工作流程:

不断调用read()→直到有返回数据

特点:CPU空转严重,实际很少使用

IO多路复用

一个线程监控多个 IO 事件,当某个 IO 就绪后再处理,是NIO的基础

Java NIO 实际为同步非阻塞IO+IO多路复用

Netty使用的是基于 epoll 的 IO 多路复用 + Reactor 模式

实际也是轮询,但是由内核实现轮询,CPU开销较低,并发能力强。

有三种实现:select/poll/epoll

实现

特点

性能

select

监听 fd 集合;有最大连接数限制;每次都需要拷贝 fd 集合

O(n)

poll

取消 fd 数量限制;本质仍是轮询

O(n)

epoll

事件驱动(回调);只关注就绪 fd;内核态维护就绪队列;Linux中使用

O(1)

信号驱动IO

数据准备好后发送信号,使用较少

异步IO(AIO)

内核完成所有操作,通过回调通知应用,Java AIO 属于这一类

2. Java BIO

BIO 是什么

Java BIO 是基于阻塞 IO 模型的同步 IO 实现

阻塞:线程在 read() / write() 时会挂起

同步:数据拷贝由调用线程完成

一连接一线程:典型并发模型

适用场景:低并发、内部工具、管理后台、简单文件处理

不适用于高并发网络服务与长连接场景

BIO 的整体工作模型

现在有一个 Java 服务端,一个客户端连进来发数据

服务端启动:

ServerSocket serverSocket = new ServerSocket(8080);

此时程序启动,只有一个主线程,没有IO发生

accept():

Socket socket = serverSocket.accept();

这里是第一个阻塞点,如果还没有客户端连接,主线程就在这里阻塞,一旦有客户端连进来,accept() 就会返回一个 Socket

拿到 Socket 后准备管道:

InputStream in = socket.getInputStream();

read():

in.read(buffer);

这里是第二个阻塞点,如果客户端还没发数据,则当前线程阻塞等待,若客户端发数据了,则内核拷贝数据,read()返回

那如果有多个客户端连接怎么办?

BIO 是一连接一线程机制:

while (true) {
    Socket socket = serverSocket.accept(); // 阻塞等待连接

    new Thread(() -> {
        try {
            InputStream in = socket.getInputStream();
            byte[] buf = new byte[1024];

            in.read(buf); // 阻塞等待数据
        } catch (IOException e) {
            e.printStackTrace();
        }
    }).start();
}

总结:Java BIO 采用的是同步阻塞模型。服务端通常使用 ServerSocket 在主线程中阻塞 accept 连接,每当有一个客户端连接,就创建一个线程专门处理该连接,线程在 read/write 时会一直阻塞等待数据。因此 BIO 是典型的一连接一线程模型,并发能力受限。

BIO 的核心问题

线程资源浪费:

大量线程处于阻塞状态

线程栈内存占用大(1MB 左右 / 线程)

上下文切换成本高:

线程越多 → 调度越频繁

C10K 问题:

1 万连接 ≈ 1 万线程 → 系统崩溃

Java IO 流体系结构

字节流:

抽象类:

InputStream

OutputStream

常见实现:

FileInputStream / FileOutputStream

BufferedInputStream / BufferedOutputStream

ObjectInputStream / ObjectOutputStream

ByteArrayInputStream / ByteArrayOutputStream

使用场景:

图片、视频、压缩包、二进制数据

字符流:

抽象类:

Reader

Writer

常见实现:

FileReader / FileWriter

BufferedReader / BufferedWriter

InputStreamReader / OutputStreamWriter

使用场景:

文本数据(涉及编码)

对比:

对比点

字节流

字符流

操作单位

byte

char

是否编码

底层

操作字节

封装字节流

适用场景

任意文件

文本

节点流、处理流

节点流:

直接连接数据源,FileInputStream、FileReader

处理流:

包装节点流,增强功能

常见:

缓冲(BufferedXXX):内部维护一个 byte[] / char[],应用与系统之间多一层缓冲,通过空间换时间,提高 IO 性能,减少系统调用,一次读取一块数据。

转换(InputStreamReader)

序列化(ObjectXXX)

例如:

BufferedReader br = new BufferedReader(
    new InputStreamReader(
        new FileInputStream("test.txt")
    )
);

字符编码 & InputStreamReader

当字节 → 字符 的编码不一致时,会出现乱码。

InputStreamReader 可以在字节流转换为字符流时指定编码:

new InputStreamReader(
    new FileInputStream("a.txt"),
    StandardCharsets.UTF_8
);

3. Java NIO

NIO 三大组件

组件

作用

Channel

连接、数据通道

Buffer

数据容器

Selector

事件通知器

IO 多路复用

一个 Selector 监控多个 Channel ,哪个 Channel 就绪了处理哪个

工作流程

程序启动:

Selector selector = Selector.open();

创建 ServerSocketChannel:

ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);

Channel 可以设置成非阻塞

把 server 注册到 selector:

Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);

集中管理事件,当有连接发生时通知Selector,此时 selector 里记录了一个Channel和一个关心的事件类型。

select():

selector.select(); // 阻塞

等待事件发生,注意Selector 阻塞的是“事件”而不是某个具体的连接

当事件发生时(一个客户端连接/已连接的客户端发送数据),OS 通知 Selector,select()返回

事件

含义

出现在哪

OP_ACCEPT

有新连接

ServerSocketChannel

OP_READ

可读

SocketChannel

OP_WRITE

可写

SocketChannel

OP_CONNECT

连接完成

SocketChannel(客户端)

处理就绪事件:

Set<SelectionKey> keys = selector.selectedKeys();

select()返回后拿到数据:哪个 Channel 发生了什么事。遍历 OP_ACCEPT 接收数据,遍历 OP_READ 读取数据

读数据:

SocketChannel channel = (SocketChannel) key.channel();
channel.read(buffer); // 没数据直接返回

其他

select() 存在空查询bug:select() 可能空轮询,导致 CPU 100%,原因为JDK epoll Bug,select() 返回 0实际无事件(Netty 通过重建 Selector 解决)

4. 零拷贝

普通IO的拷贝过程

普通IO一共要经过四次拷贝:

磁盘 → 内核缓冲区:DMA 把数据从磁盘读到 内核 buffer

内核缓冲区 → JVM 堆(byte[]):read() 把数据拷到 byte[]

JVM 堆 → Socket 内核缓冲区:write() 再拷一遍

Socket 缓冲区 → 网卡:DMA 发送

零拷贝的思想

在普通IO的四次拷贝中,从内核缓冲区到JVM、从JVM堆到Socket内存缓冲区的两次拷贝是多余的,零拷贝省去这两次拷贝,数据不再经过用户态,直接在内核态完成“文件 → 网络”的转发。

适用于大文件传输,日志同步,MQ消息转发等。

为什么零拷贝能提升性能?

减少了CPU 拷贝次数,无需用户态 ↔ 内核态切换,减少了 CPU Cache 污染

零拷贝的使用

普通IO的使用方式:

FileInputStream fis = new FileInputStream("a.txt");
SocketOutputStream out = socket.getOutputStream();

byte[] buf = new byte[8192];
while ((len = fis.read(buf)) != -1) {
    out.write(buf, 0, len);
}

零拷贝使用FileChannel.transferTo / transferFrom:

FileChannel fileChannel = new FileInputStream("a.txt").getChannel();
SocketChannel socketChannel = SocketChannel.open();

fileChannel.transferTo(0, fileChannel.size(), socketChannel);

使用 transferTo后,拷贝过程变为:磁盘 → 内核缓冲区 → Socket 缓冲区 → 网卡

没有 JVM ↔ 内核来回拷贝

还有一种DirectByteBuffer(堆外内存)的使用:

ByteBuffer buffer = ByteBuffer.allocateDirect(8192);

堆外内存不是完全零拷贝,但也减少了拷贝次数。其内存不在 JVM 堆,可以被内核直接访问,少一次:JVM堆 → 内核 的拷贝

底层原理

零拷贝靠的是这两种机制之一:

sendfile(真正的零拷贝):由Linux 提供,内核直接把文件发到 socket,CPU 几乎不参与

mmap(内存映射):文件映射到内存,可以减少一次拷贝

具体使用哪种,由 Java 根据平台选择决定

5. Reactor 模式

Reactor 模式是什么

Reactor 模式是一种基于事件驱动的并发处理模型,它通过一个或多个 Reactor 线程监听 IO 事件,将就绪事件分发给对应的处理器,从而实现高并发网络通信。

解决了BIO一连接一线程,线程阻塞的问题,直接 NIO,IO 线程被业务逻辑拖慢的问题。将 IO 监听与业务处理解耦,用少量 IO 线程支撑大量并发连接。

核心组成

组件

职责

Reactor

监听事件、分发事件

Selector

IO 多路复用器

Channel

网络连接

Event Handler

事件处理器

Worker 线程池

业务处理

工作流程

Channel 注册到 Selector →

Reactor 线程调用 select() 等待事件 →

事件就绪后,获取 SelectionKey →

根据事件类型分发给 Handler →

Handler 进行处理或提交给线程池

Reactor 只负责“通知和分发”,不负责具体业务。

这种设计使得 Reactor 不会被某个较慢的具体业务拖慢性能。

三种 Reactor 模型

单 Reactor 单线程

一个 Reactor 线程同时负责 IO 事件监听、事件分发以及业务处理。

线程结构:

1 个 Reactor 线程
- select
- accept
- read
- write
- 业务逻辑

实现简单,但当任一业务阻塞就会阻塞所有连接

单 Reactor 多线程

一个 Reactor 线程负责 IO 事件监听和分发,业务处理交由 Worker 线程池完成。

1 个 Reactor(IO)
N 个 Worker(业务)

IO 线程不阻塞,但 accept 和 IO 仍可能成为瓶颈,适用于大多数 Web 应用

主从 Reactor 多线程

通过多个 Reactor 分工协作,主 Reactor 负责连接接入,从 Reactor 负责 IO 读写,业务逻辑由独立线程池处理。

线程结构:

Main Reactor(accept)
Sub Reactor(read / write)
Worker Pool(业务)

并发能力最强,但实现复杂

Netty、Tomcat NIO、Kafka 使用了这种模式

Reactor 与 BIO 与 NIO

对比点

BIO

Reactor

IO 模型

阻塞

非阻塞

线程模型

一连接一线程

少量 IO 线程

并发能力

资源利用

Java NIO 提供了 Channel、Selector 等底层工具,Reactor 模式则定义了如何使用这些工具来构建高并发服务器。Reactor 是一种设计模式,而 NIO 是具体的 API。

6. Java AIO

AIO 是什么

Java AIO 是基于异步 IO 模型的非阻塞 IO 实现,应用发起 IO 请求后立即返回,IO 操作由操作系统完成,并通过回调通知应用结果。

AIO 与 NIO 的本质区别

对比点

NIO

AIO

IO 类型

同步非阻塞

异步

数据拷贝

应用线程

内核

通知方式

轮询 / 事件

回调

编程模型

Reactor

Proactor

工作流程

应用发起异步 IO 请求 →

方法立即返回 →

内核完成 IO 操作 →

回调 CompletionHandler →

应用处理结果

优缺点

优点是真正异步,理论并发能力强,无需 select / 轮询

缺点是操作系统支持有限(很多实现是“线程 + 回调”的假异步,本质仍是阻塞 IO 封装),编程复杂度高且生态不成熟

生产环境很少使用AIO:Java AIO 在 Linux 平台上底层支持不完善,网络 IO 实现效果有限,实际生产中更常用 NIO + Reactor + Netty 的组合,兼顾性能、稳定性和生态成熟度。

核心 API

核心 Channel:

AsynchronousSocketChannel

AsynchronousServerSocketChannel

AsynchronousFileChannel

CompletionHandler:

channel.read(buffer, attachment, new CompletionHandler<Integer, Object>() {
    @Override
    public void completed(Integer result, Object attachment) {
        // IO 完成后的回调
    }

    @Override
    public void failed(Throwable exc, Object attachment) {
        // IO 失败
    }
});

IO 完成后才回调,不阻塞任何线程

AIO vs Reactor

对比点

Reactor

Proactor

IO 模型

同步非阻塞

异步

数据拷贝

应用

内核

代表

NIO

AIO

使用

广泛

7. Netty

Netty 是什么

Netty 是一个基于 Java NIO 的高性能网络通信框架,通过封装 Reactor 模型、内存管理、线程模型和零拷贝机制,简化并优化了高并发网络编程。

原生NIO存在以下问题:

API 复杂:状态多、代码冗长

易出 Bug:Selector 空轮询、OP_WRITE 滥用

无协议支持:粘包/半包自己处理

内存管理差:ByteBuffer 不好用

线程模型要自研:Reactor 要自己实现

而 Netty 提供了解决方案,封装 Reactor 模型,高性能 ByteBuf,内存池,零拷贝,Pipeline 责任链,解决了 JDK NIO Bug。

Netty 的 IO 线程模型

boss / worker 线程模型(主从 Reactor 多线程模型):

BossGroup(Main Reactor)
  - 接收连接(OP_ACCEPT)

WorkerGroup(Sub Reactor)
  - 处理 IO(OP_READ / OP_WRITE)

当发生一次请求时:

Boss 接收连接

→ 分配 SocketChannel 给 Worker

→ Worker 监听读写事件

→ 读到数据后,进入 Pipeline

→ 业务 Handler 处理

→ 写回响应

优点:accept 不阻塞 IO,IO 不阻塞业务,线程职责清晰,易扩展

Netty 的 Channel & EventLoop

EventLoop = 一个线程 + 一个 Selector + 一个任务队列

一个 Channel 始终绑定同一个 EventLoop,可以避免并发问题,也无需加锁

而EventLoopGroup管理多个 EventLoop,提供线程池能力

ByteBuf vs ByteBuffer

ByteBuffer 的问题:

问题

ByteBuffer

扩容

不支持

读写指针

共用

使用体验

容易出错

池化

ByteBuf 的优势:

特性

说明

读写指针分离

readerIndex / writerIndex

自动扩容

不用手动 flip

池化

减少 GC

零拷贝支持

slice / composite

风险

如果 Handler 没正确释放(ReferenceCountUtil.release(msg)),会导致内存泄漏。

零拷贝

Netty 的零拷贝是“多种技术组合”,不是一种实现方式。

常见方式有:

FileRegion(sendfile):

文件 → socket

基于 FileChannel.transferTo

CompositeByteBuf:

多个 ByteBuf 逻辑合并

不发生真实拷贝

slice / duplicate:

共享底层内存

只改索引

Pipeline & Handler

Pipeline 是一个双向责任链,用于处理入站和出站事件。

使用 Pipeline 可以解耦协议处理,解耦业务逻辑,实现可插拔

而常见 Handler 有解码器、编码器、业务处理器

Netty 如何解决 NIO 的问题

NIO 问题

Netty 解决方案

Selector 空轮询

重建 Selector(在 Linux 下,当 epoll 触发了一个无意义的轮询(其实没有事件),会导致 select() 不再阻塞。Netty 的算法是:统计 select() 次数,如果在极短时间内连续返回(默认 512 次),就判定触发了 Bug,并进行重建。)

OP_WRITE 空转

精确控制

Buffer 管理

ByteBuf + 池化

线程模型复杂

boss / worker

粘包问题

编解码器

Netty vs Tomcat NIO

对比点

Tomcat NIO

Netty

定位

Web 容器

网络框架

灵活性

一般

协议

HTTP

任意

使用复杂度

8. BIO / NIO / AIO 对比

对比

BIO

NIO

AIO

阻塞

同步

并发

更高

使用