Java IO 模型 #

BIO、NIO、AIO 三种IO模型对比,Netty 核心概念,面试高频考点


📋 目录 #


IO 基础概念 #

核心概念 #

概念 说明
阻塞IO 发起IO请求后,线程挂起等待结果,什么都不做
非阻塞IO 发起IO请求后,线程继续执行,轮询检查是否就绪
同步IO IO操作由应用线程主动完成
异步IO IO操作由内核完成,完成后通知应用

Unix IO 模型分类 #

IO 执行分两步:
1. 等待数据就绪(数据从网卡到内核缓冲区)
2. 把数据从内核拷贝到用户进程缓冲区

不同模型区别就在这两步的处理方式

BIO 阻塞IO #

模型图 #

线程accept

阻塞等待连接

有连接?

开新线程处理IO

线程read阻塞等待数据

处理数据

返回结果

代码示例 #

ServerSocket serverSocket = new ServerSocket(8080);
while (!closed) {
    Socket socket = serverSocket.accept(); // 阻塞等待连接
    new Thread(() -> {
        try {
            byte[] buf = new byte[1024];
            int len = socket.getInputStream().read(buf); // 阻塞等待数据
            // 处理数据...
        } catch (IOException e) {
            e.printStackTrace();
        }
    }).start();
}

特点 #

  • 优点:实现简单
  • 缺点:一个连接一个线程,高并发下线程太多,内存和上下文切换开销大
  • 适用场景:连接数少、固定架构,传统阻塞服务

NIO 非阻塞IO #

核心组件 #

Channel (通道)
  ↓
Buffer (缓冲区)
  ↓
Selector (选择器)
组件 作用
Channel 双向数据通道,代替传统Stream
Buffer 缓冲区,数据读写都经过Buffer
Selector 多路复用器,一个线程监听多个Channel的就绪事件

多路复用模型图 #

Selector 线程

select() 阻塞等待事件

遍历 SelectionKey 就绪事件

accept?

注册Channel到Selector

read?

读取数据处理

代码示例 #

ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false); // 非阻塞

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

while (!closed) {
    selector.select(); // 阻塞等待就绪事件
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) {
            // 处理新连接
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            // 读取数据
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buf = ByteBuffer.allocate(1024);
            int len = client.read(buf);
            // 处理数据...
        }
    }
    keys.clear();
}

Buffer 读写模式 #

写模式

flip()

读模式

clear()/compact()

Buffer 属性:

  • capacity:缓冲区总容量
  • position:当前读写位置
  • limit:读写限制(写模式=capacity,读模式=写入位置)

特点 #

  • 一个线程处理多个连接,线程开销小
  • 非阻塞,连接没数据不会阻塞线程
  • 基于事件驱动,适合高并发场景
  • 编程复杂度比BIO高很多

AIO 异步IO #

模型概念 #

发起异步read

立即返回,继续处理其他事

内核等待数据就绪

内核拷贝完数据

回调通知应用线程处理

  • 用户线程发起请求后继续执行,不等待
  • 内核完成IO后回调用户线程处理
  • CompletableFutureCompletionHandler 回调

代码示例 #

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

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 len, ByteBuffer buf) {
                // 处理数据...
            }
            @Override
            public void failed(Throwable exc, ByteBuffer buf) {
                // 处理失败
            }
        });
    }
    @Override
    public void failed(Throwable exc, Void attachment) {
        // 处理失败
    }
});

特点 #

  • 真正的异步,内核完成后回调
  • 理论上性能更高
  • Linux 上 AIO 实现不够完善,实际用得少
  • Netty 也没有基于 AIO

三种模型对比 #

对比项 BIO NIO AIO
IO方式 同步阻塞 同步非阻塞(多路复用) 异步非阻塞
线程模型 一个连接一线程 一个线程多连接 内核完成回调
编程复杂度 简单 复杂 更复杂
并发性能 理论最高
实际应用 低并发场景 主流(Netty) 很少用

适用场景 #

  • BIO:连接数少,业务简单,传统Tomcat BIO模式
  • NIO:高并发、长连接,Netty、Jetty、Tomcat NIO模式
  • AIO:连接数极多,windows平台支持好,Linux实际少用

Netty 核心概念 #

Netty 是什么? #

  • 基于 NIO 的高性能网络通信框架
  • 封装了JDK NIO 的复杂性,提供易用API
  • 解决了TCP粘包拆包问题
  • 内置多种编解码,支持HTTP、WebSocket等协议

核心组件 #

组件 作用
EventLoop 事件循环线程,处理IO事件
EventLoopGroup 事件循环线程组
Channel Netty 对 Socket 的封装
ChannelHandler 业务处理器,处理入站出站事件
ChannelPipeline Handler 链,责任链模式
ByteBuf Netty 缓冲区,替代 NIO ByteBuffer

线程模型 #

accept连接

BossGroup

注册到WorkerGroup

EventLoop 线程

处理Channel所有事件

  • BossGroup:专门处理accept连接
  • WorkerGroup:处理连接的读写事件
  • 一个EventLoop线程处理多个Channel

零拷贝 #

Netty 零拷贝优化:

  1. CompositeByteBuf:组合多个缓冲区,避免数据拷贝
  2. FileRegion:sendfile 系统调用,直接从内核到网卡,不经过用户空间
  3. 直接内存:使用堆外内存,避免GC拷贝

面试题汇总 #

基础 #

  1. BIO、NIO、AIO 区别?

    答案:

    维度 BIO NIO AIO
    IO类型 同步阻塞 同步非阻塞(多路复用) 异步非阻塞
    线程模型 一个连接对应一个线程 一个线程处理多个连接 内核完成后回调
    编程复杂度 简单 复杂 更复杂
    适用场景 连接数少且固定 高并发、长连接 理论性能最高,实际少用

    核心差异:

    • BIO:accept() 和 read() 都会阻塞,线程只能等,一个连接独占一个线程
    • NIO:通过 Selector 多路复用,一个线程可以监听多个 Channel 的就绪事件
    • AIO:真正异步,IO操作完全交给内核,完成后回调通知
  2. NIO 的 Channel、Buffer、Selector 各自作用?

    答案:

    组件 作用
    Channel 双向数据通道,代替传统的 InputStream/OutputStream。可以读写、支持非阻塞
    Buffer 缓冲区,数据读写都要经过 Buffer。本质是一块内存,有 position、limit、capacity 三个关键属性
    Selector 多路复用器,一个线程可以监听多个 Channel 的事件(accept、connect、read、write),实现单线程处理多连接

    三者关系: Selector → Channel → Buffer,Selector 注册 Channel,Channel 读写 Buffer

  3. 什么是IO多路复用?

    答案:

    核心思想: 一个线程可以同时监听多个文件描述符(Socket)的就绪状态,哪个就绪就处理哪个,避免为每个连接开一个线程。

    关键点:

    • 单线程/少量线程监听多个连接
    • 通过 select/poll/epoll 等系统调用实现
    • 就绪事件驱动,有事件才处理,没事件就阻塞或干别的

    类比: 餐厅服务员同时照看多桌客人,哪桌喊"买单"就去哪桌处理,而不是每桌配一个专属服务员

NIO #

  1. NIO 为什么能单线程处理多连接?

    答案:

    两个核心机制:

    ① 非阻塞 Channel

    • Channel 配置为 configureBlocking(false)
    • accept()read()write() 都立即返回,不会阻塞线程

    ② Selector 多路复用

    • 把多个 Channel 注册到同一个 Selector
    • 调用 select() 阻塞等待就绪事件
    • 事件就绪后,遍历 selectedKeys 逐个处理

    结果: 单线程可以同时监听几百上千个连接,哪个连接有数据才处理哪个,避免了 BIO 一连接一线程的开销

  2. ByteBuffer flip() 方法作用是什么?

    答案:

    切换 Buffer 从写模式到读模式

    写模式: 往 Buffer 写数据

    • position 指向当前写入位置
    • limit = capacity

    flip() 做了两件事:

    1. limit = position → 可读上限设为刚才写入的位置
    2. position = 0 → 读指针移到开头

    典型用法:

    buf.put(data); // 写入数据
    buf.flip();    // 切换读模式
    channel.read(buf); // 读取
    buf.clear();   // 清空复位
    

    相关方法:

    • clear():清空缓冲区,position=0,limit=capacity(写模式)
    • compact():把未读完的数据移到开头,继续写(部分清空)
  3. Selector 原理,select/poll/epoll 区别?

    答案:

    Selector 原理:

    • 基于操作系统的多路复用机制(select/poll/epoll)
    • 把多个 Channel 注册到 Selector 上
    • select() 调用阻塞等待事件,有事件就绪就返回
    • 遍历 selectedKeys 处理每个就绪事件

    select/poll/epoll 对比:

    特性 select poll epoll
    最大连接数 有限(1024/2048) 无限制 无限制
    性能 O(n) 遍历所有fd O(n) 遍历所有fd O(1) 回调就绪
    内存拷贝 每次调用拷贝fd集合 每次调用拷贝fd集合 共享内存,无需拷贝
    触发方式 水平触发 水平触发 水平/边缘触发
    平台 跨平台 跨平台 Linux 专用

    水平触发 vs 边缘触发:

    • 水平触发 (LT):只要数据就绪就持续通知,直到处理完
    • 边缘触发 (ET):只在状态变化时通知一次,必须一次性读完

Netty #

  1. Netty 的线程模型?为什么分 BossGroup 和 WorkerGroup?

    答案:

    Netty 线程模型(主从 Reactor 多线程):

    BossGroup (1-N 个 EventLoop)
        ↓ accept 连接
    WorkerGroup (N 个 EventLoop)
        ↓ 处理读写事件
    ChannelPipeline (Handler 链)
    

    BossGroup 职责:

    • 只负责 accept 新连接
    • 接受连接后注册到 WorkerGroup
    • 通常 1-2 个线程足够

    WorkerGroup 职责:

    • 处理已建立连接的所有 IO 事件(read/write)
    • 执行 ChannelPipeline 中的业务 Handler
    • 线程数通常 = CPU 核心数 × 2

    为什么分离?

    • 职责单一:accept 是低频操作,read/write 是高频操作
    • 性能优化:Boss 只处理连接建立,Worker 专注于业务处理
    • 独立伸缩:可以独立配置两组的线程数
  2. Netty 如何解决 TCP 粘包拆包问题?

    答案:

    TCP 粘包拆包原因:

    • TCP 是流式协议,没有消息边界
    • 发送方可能合并多个小包发送(Nagle 算法)
    • 接收方可能一次读取多个包

    Netty 内置解决方案:

    解码器 原理 适用场景
    LineBasedFrameDecoder 按换行符 \n\r\n 分割 每行一条消息,如 HTTP、Telnet
    DelimiterBasedFrameDecoder 按自定义分隔符分割 有特殊分隔符的协议
    FixedLengthFrameDecoder 固定长度分割 每条消息长度固定
    LengthFieldBasedFrameDecoder 读取消息头中的长度字段 自定义协议(最常用)

    LengthFieldBasedFrameDecoder 例子:

    +--------+----------------+
    | Length | Actual Content |
    | 4 bytes| "HELLO, WORLD" |
    +--------+----------------+
    

    配置:maxFrameLength=1024, lengthFieldOffset=0, lengthFieldLength=4

  3. Netty 的零拷贝实现原理?

    答案:

    零拷贝(Zero-Copy):减少或避免 CPU 拷贝数据的次数,提升性能

    Netty 三种零拷贝方式:

    ① CompositeByteBuf - 逻辑组合

    • 把多个 ByteBuf 组合成一个逻辑视图
    • 不需要物理拷贝数据
    • 例如:header + body 两个缓冲区,组合成一个处理

    ② FileRegion - sendfile 系统调用

    • 利用 Linux sendfile() 直接把文件从内核缓冲区发送到网卡
    • 不经过用户空间,减少一次拷贝
    • 适用于文件传输

    ③ 堆外内存(Direct Buffer)

    • ByteBuf 支持直接分配堆外内存
    • 避免 Java 堆到 native 堆的拷贝
    • 减少 GC 压力

    传统 IO vs Netty 零拷贝:

    • 传统:磁盘 → 内核缓冲区 → 用户缓冲区 → Socket → 网卡 (4次拷贝)
    • Netty:磁盘 → 内核缓冲区 → Socket → 网卡 (2次拷贝)
  4. Netty 中 ByteBuf 和 JDK ByteBuffer 区别?

    答案:

    特性 JDK ByteBuffer Netty ByteBuf
    读写模式 需要 flip() 切换 自动双指针,无需切换
    指针 一个 position readerIndex + writerIndex
    扩容 固定大小,需手动 自动扩容,类似 ArrayList
    内存类型 heap/direct heap/direct/composite
    池化 PooledByteBufAllocator 支持池化
    API 较简陋 丰富的工具方法

    ByteBuf 核心优势:

    • 双指针:读用 readerIndex,写用 writerIndex,互不干系,无需 flip
    • 自动扩容:写超了自动扩,不用手动 reallocate
    • 池化:复用缓冲区,减少 GC 和分配开销
    • 组合缓冲区:CompositeByteBuf 可以逻辑组合多个缓冲区

    典型代码对比:

    // JDK ByteBuffer
    ByteBuffer buf = ByteBuffer.allocate(1024);
    buf.put(data);
    buf.flip(); // 必须手动切换!
    // Netty ByteBuf
    ByteBuf buf = Unpooled.buffer();
    buf.writeBytes(data);
    buf.readBytes(out); // 无需 flip!
    

🔗 相关笔记 #


最后更新: 2026-04-29