05-IO与NIO

前言

本文件汇总专题「05-IO与NIO」,收录该目录下的所有 Markdown 原文,并提供可点击目录便于跳转查阅。

目录

Java面试题合集-42-BIO-NIO-AIO区别.md

BIO / NIO / AIO 的区别与适用场景?

这题如果只背一句“BIO 阻塞、NIO 非阻塞、AIO 异步”,面试官通常会追问:

你说的“阻塞/非阻塞/异步”到底阻塞在哪里?谁在等谁?
NIO 为什么还要 Selector?它真的是“完全不阻塞”吗?

这篇用“人话 + 图”把三者关系讲清楚。


1)一句话结论

BIO 是线程阻塞等待 IO;NIO 是基于 Selector 的 IO 多路复用,单线程可管理多连接,读写调用仍可能发生“数据未就绪”的非阻塞返回;AIO 是真正的异步 IO(由系统完成 IO 后回调通知),应用线程不需要轮询等待。选型看连接数、并发模型、延迟/吞吐目标与平台支持。


2)先把三个概念分开:同步/异步 vs 阻塞/非阻塞

很多人把它们混在一起。

  • 阻塞/非阻塞:调用 read/write 时,线程会不会卡住等待?
  • 同步/异步:IO 的完成通知方式是谁来“确认完成”?
    • 同步:你自己去问(调用返回时你知道结果)
    • 异步:别人做完告诉你(回调/事件通知)

所以常见组合是:

  • BIO:同步 + 阻塞
  • NIO:同步 + 非阻塞(配合多路复用)
  • AIO:异步(通常表现为非阻塞)

3)BIO:一个连接一个线程(简单但不抗连接数)

Client1 ──> [Thread-1] 阻塞 read
Client2 ──> [Thread-2] 阻塞 read
Client3 ──> [Thread-3] 阻塞 read
...

优点:

  • 代码简单直观
  • 适合连接数少、并发不高的场景

缺点:

  • 连接数上来线程数爆炸:线程栈内存、上下文切换成本很高
  • 大量线程阻塞等待,资源利用率差

适用:

  • 内部工具、低并发服务、连接数很少的场景

4)NIO:一个线程看很多连接(Selector 多路复用)

NIO 的关键不是“完全不阻塞”,而是:

让一个线程同时管理很多连接,只在“有事件发生”的时候再处理。

            ┌───────────────┐
Channel1 ──►│               │
Channel2 ──►│   Selector    │──► 事件就绪后再处理
Channel3 ──►│               │
            └───────────────┘

优点:

  • 少量线程支撑大量连接(高并发连接)
  • 适合高连接数、短任务处理的网络服务

注意点(面试加分):

  • NIO 的 read/write 是“非阻塞调用”,可能读到 0 字节/写不完,需要配合状态机与缓冲区处理
  • 复杂度高于 BIO:半包/粘包、事件循环、Buffer 管理

适用:

  • 高连接数(长连接)场景:IM、网关、推送
  • 需要更高吞吐/更少线程的网络服务

5)AIO:我去忙,你做完叫我(回调/CompletionHandler)

应用线程:提交 IO 请求 ──► OS/内核执行 IO ──► 完成后回调通知

优点:

  • 线程不需要轮询就绪事件
  • 理论上更“异步友好”

现实提醒(更像工程师的回答):

  • AIO 的收益依赖操作系统与实现质量;在 Java 生态中网络编程主流仍是 NIO + Netty

适用:

  • 需要异步模型且平台支持良好的场景

6)选型建议(面试可直接给)

连接数少 + 简单优先:BIO(或线程池 + BIO)
连接数多 + 高并发网络:NIO(生产常用 Netty)
异步回调模型 + 平台支持好:AIO(了解即可)

7)一句话收尾(面试可直接用)

BIO 以线程阻塞等待 IO,简单但线程成本高;NIO 用 Selector 做多路复用,少量线程管理大量连接,但需要处理非阻塞读写与状态机;AIO 是异步完成通知模型,线程不等待,由系统完成后回调。生产网络服务通常以 NIO/Netty 为主。

Java面试题合集-43-零拷贝是什么.md

什么是零拷贝?在 Java/Netty 中如何体现?

零拷贝(Zero-Copy)听起来像“完全不拷贝”,其实更准确的理解是:

尽量减少不必要的内存拷贝与上下文切换,让数据更少地在“用户态/内核态/缓冲区”之间来回搬家。


1)一句话结论

零拷贝是通过 DMA、内核缓冲区复用、sendfile、mmap 等机制减少用户态与内核态之间的数据拷贝次数和上下文切换;在 Java/Netty 中常见体现包括 FileChannel.transferTo/transferFrom(sendfile 路径)、DirectBuffer(减少一次拷贝)、以及 ByteBuf 的 slice/retain(减少数据复制)。


2)先看“传统拷贝”到底拷了几次

以“读文件 → 发到网络”为例(简化示意):

磁盘 --DMA--> 内核缓冲区
内核缓冲区 --copy--> 用户缓冲区(read)
用户缓冲区 --copy--> 内核 socket 缓冲区(write)
内核 socket 缓冲区 --DMA--> 网卡

你会看到至少两次“内核<->用户”的 copy(加上 DMA),这就是传统路径的成本来源。


3)sendfile:把“用户态那一段搬运”省掉

sendfile(概念示意):

磁盘 --DMA--> 内核缓冲区
内核缓冲区 ---------> 内核 socket 缓冲区(内核内转移/引用)
内核 socket 缓冲区 --DMA--> 网卡

应用线程不再需要把数据 read 到用户态再 write 回内核态。

Java NIO 里常见入口:

  • FileChannel.transferTo(...)
  • FileChannel.transferFrom(...)
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class TransferToDemo {
    static void sendFile(SocketChannel sc, String path) throws Exception {
        try (RandomAccessFile raf = new RandomAccessFile(path, "r");
             FileChannel fc = raf.getChannel()) {
            long pos = 0;
            long size = fc.size();
            while (pos < size) {
                pos += fc.transferTo(pos, size - pos, sc);
            }
        }
    }
}

4)mmap:把文件映射到内存(减少拷贝但要注意资源)

mmap 的思路是把文件映射到进程地址空间,减少显式 copy,但它不是“免费的午餐”:

  • 映射/页面回收有成本
  • 大量 mmap 可能带来地址空间压力
  • 需要注意释放时机(Java 场景有历史坑)

面试里能说到“mmap 也是零拷贝思路之一,但要谨慎使用”就很稳。


5)Netty 里的“零拷贝”更多指“减少用户态内存复制”

Netty 常见的零拷贝手段(面试加分点):

5.1 ByteBuf 的 slice/duplicate(共享底层内存)

// 伪代码:slice 产生视图,不复制数据
ByteBuf buf = ...;
ByteBuf header = buf.slice(0, 10);
ByteBuf body = buf.slice(10, buf.readableBytes() - 10);

这类操作减少了“new byte[] 再拷贝”的成本。

5.2 CompositeByteBuf(逻辑合并,多段拼装)

把多个 ByteBuf 组合成一个“逻辑上的连续缓冲”,避免拷贝拼接。

5.3 DirectBuffer(直接内存)

DirectBuffer 能减少一次“JVM 堆 → 内核”的拷贝(具体取决于实现),但要注意:

  • 直接内存也是内存,过量会 OOM(Direct buffer memory)

6)一句话收尾(面试可直接用)

零拷贝的目标是减少内存拷贝和上下文切换;Java 里 FileChannel.transferTo 常走 sendfile 路径,Netty 里通过 ByteBuf 的 slice/CompositeByteBuf/DirectBuffer 等减少数据复制与搬运。它提升吞吐与降低延迟,但仍需关注平台支持与内存管理成本。

Java面试题合集-44-Netty线程模型.md

Netty 线程模型是什么?为什么性能好?

Netty 的性能好,很多时候不是“它写得更快”,而是:

它把网络 IO 这类高并发场景的线程与事件组织方式做对了。


1)一句话结论

Netty 基于 Reactor 模型:Boss 线程负责 accept,新连接注册到 Worker(EventLoopGroup),Worker 的 EventLoop 负责该连接的读写事件与 Pipeline 回调;EventLoop 单线程串行处理同一 Channel 的事件,减少锁竞争,同时利用 NIO 的 Selector 少线程管理多连接,因此吞吐高、延迟更稳定。


2)先用一张图记住 Boss/Worker

          ┌──────────────┐
Client -> │   BossGroup  │  负责 accept
          └──────┬───────┘
                 │ 注册新连接到某个 Worker
          ┌──────▼──────────────────────────┐
          │           WorkerGroup            │
          │  EventLoop1  EventLoop2  ...     │
          └──────┬──────────┬───────────────┘
                 │          │
              ChannelA   ChannelB   (一个 Channel 绑定一个 EventLoop)

核心点:

  • Boss:接新客(accept)
  • Worker:服务已入座的客(read/write)

3)EventLoop:为什么“单线程串行”反而更稳?

Netty 的关键设计之一是:

同一个 Channel 的 IO 事件与 handler 回调由同一个 EventLoop 串行执行。

收益:

  • 少锁:同一连接内的业务处理通常不需要到处加锁
  • 少切换:事件循环模型减少线程上下文切换

注意:它不是“全系统只有一个线程”,而是“每个 EventLoop 单线程串行处理其负责的一批连接”。


4)Pipeline:把网络处理拆成可插拔的“流水线”

Netty 处理链通常是:

ByteBuf 入站 -> decoder -> handler -> encoder -> ByteBuf 出站

工程收益:

  • 每个 handler 只做一件事(解码/鉴权/业务/编码)
  • 易复用、易插拔(非常适合做网关、协议栈)

5)为什么性能好?(面试说 4 点就够)

1)NIO + Selector:少线程支撑多连接
2)Reactor 模型:事件驱动,避免每连接一线程
3)EventLoop 串行:减少锁竞争与线程切换
4)ByteBuf 与内存池:减少内存分配与拷贝(高频收发时收益明显)


6)面试追问:业务 handler 里能不能做耗时操作?

能,但不建议直接在 IO 线程(EventLoop)里做长时间阻塞:

  • 会阻塞该 EventLoop 负责的所有连接的 IO 事件
  • 导致延迟飙升

正确做法:

  • 把耗时任务丢到业务线程池(异步执行)
  • IO 线程只做快速处理与派发

7)一句话收尾(面试可直接用)

Netty 采用 Reactor 线程模型:Boss accept、Worker 负责读写与 Pipeline 回调;EventLoop 单线程串行处理同一连接减少锁竞争,配合 Selector 少线程支持多连接,并通过 ByteBuf/内存池降低拷贝与分配开销,因此性能与延迟表现优秀;耗时业务应异步转移到业务线程池避免阻塞 EventLoop。

Java面试题合集-45-Selector原理.md

Selector 的工作原理是什么?

如果你把 NIO 想成“一个客服同时看很多聊天窗口”,那 Selector 就是客服旁边的“消息提醒面板”:

哪个窗口来消息了、哪个窗口能发送了、哪个窗口断开了——它统一告诉你,你再去处理。


1)一句话结论

Selector 是 Java NIO 的多路复用器,允许一个线程监听多个 Channel 的事件(OP_ACCEPT/OP_READ/OP_WRITE 等);底层通常依赖操作系统的 select/poll/epoll/kqueue 等机制。应用通过 select() 阻塞等待就绪事件,然后遍历 selectedKeys 处理对应 Channel,从而实现少线程支撑高并发连接。


2)Selector 的核心对象关系

Selector
  ├─ SelectionKey( channelA, interestOps=READ )
  ├─ SelectionKey( channelB, interestOps=READ|WRITE )
  └─ ...

你在注册时告诉 Selector:

  • 我关心哪些事件(interestOps)

Selector 在 select 之后告诉你:

  • 哪些 Channel 就绪了(selectedKeys)

3)最小使用流程(面试讲步骤最关键)

1)open Selector
2)Channel 配置为 non-blocking
3)channel.register(selector, OP_READ/OP_ACCEPT/...)
4)while(true):
     selector.select()
     遍历 selectedKeys
     根据 key.isReadable/isWritable/isAcceptable 处理
     移除 key(避免重复处理)

示意代码(精简版):

while (true) {
    selector.select(); // 阻塞等待就绪事件
    var it = selector.selectedKeys().iterator();
    while (it.hasNext()) {
        var key = it.next();
        it.remove();
        if (key.isReadable()) {
            // read...
        }
    }
}

4)为什么 Selector 能提升并发?

BIO 模型中,一个连接一个线程,线程大量阻塞等待。
Selector 模型中,一个线程“统一等待多个连接的事件”,就绪才处理:

线程不需要为“没有数据的连接”干等

所以它更适合:

  • 大量长连接
  • 事件驱动的网络服务(IM/网关/推送)

5)面试加分:OP_WRITE 为什么容易踩坑?

很多人以为“要写就注册 OP_WRITE”,但实际:

  • Socket 缓冲区很多时候是可写的,OP_WRITE 可能持续触发
  • 如果你一直对 OP_WRITE 感兴趣,会造成事件循环空转(CPU 飙高)

正确思路:

  • 只有当你“真的写不完,需要等缓冲区可写”时才关注 OP_WRITE
  • 写完就取消 OP_WRITE 关注

6)一句话收尾(面试可直接用)

Selector 通过 OS 多路复用让单线程监听多个 Channel 事件:注册 interestOps,select 阻塞等待就绪,遍历 selectedKeys 处理;它减少了每连接一线程的阻塞等待,适合高连接数长连接场景。注意 OP_WRITE 需要按需注册,避免空转。

Java面试题合集-46-序列化方案怎么选.md

常见序列化方案怎么选?(JDK/JSON/Protobuf)

序列化这题面试官最想听你说清楚三件事:

1)你要给谁用(前端/服务间/落库)
2)你最在意什么(可读性/性能/兼容性)
3)你能不能避免“反序列化安全坑”


1)一句话结论

选择序列化方案要权衡可读性、体积、性能、跨语言与兼容性:JSON 可读性好、跨语言强但体积大且解析成本高;Protobuf 体积小、性能好、跨语言且有 schema,但不直观;JDK 原生序列化使用方便但性能与安全性口碑差,生产中通常不推荐对外使用。服务间通信常用 Protobuf/JSON,落库更看可演进性与检索需求。


2)三类方案对比表(面试直接用)

方案 可读性 体积 性能 跨语言 兼容性 常见场景
JDK 序列化 中/大 一般 一般 Java 内部临时(不推荐对外)
JSON 很好 一般 很强 较好(字段可选) 前后端、开放接口、日志
Protobuf 很强 很好(schema 演进) RPC/微服务内部通信

3)JSON:通用但别滥用

优点:

  • 人能读、能调试
  • 前后端天然友好

缺点:

  • 体积大,解析耗 CPU
  • 类型信息弱(需要约定字段)

工程建议:

  • 对外接口用 JSON 合适
  • 内部高频 RPC 可考虑 Protobuf

4)Protobuf:小、快、可演进

优点:

  • 二进制体积小,序列化/反序列化快
  • Schema 明确,字段可选/新增字段对兼容友好
  • 跨语言成熟

缺点:

  • 不直观,不适合直接当日志
  • 需要维护 .proto(但这通常是优势)

面试加分点:

Protobuf 适合服务间高频通信与带宽敏感场景;字段演进要遵循“不复用 tag、不随便改类型”的规则。


5)JDK 原生序列化:为什么生产不推荐?

常见原因(说 2 个就够):

  • 性能与体积表现一般
  • 历史上反序列化安全问题多(对不可信输入极其危险)

稳妥说法:

JDK 序列化不要用于接收不可信数据;生产更倾向使用 JSON/Protobuf,并做白名单/类型限制等防护。


6)怎么选?给一个“按场景”的决策树

要给前端/开放接口? -> JSON
服务间高频通信/带宽敏感? -> Protobuf
只在本进程内缓存/临时? -> 看场景(一般也能用 JSON/自定义结构),避免 JDK 序列化对外暴露

7)一句话收尾(面试可直接用)

JSON 胜在可读与生态,Protobuf 胜在体积/性能/可演进,JDK 序列化不建议对外使用且要警惕反序列化安全;选型按使用方(对外/对内)、性能与带宽目标、以及兼容演进需求综合决定。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐