【019】IO/NIO 概念:Web 开发要掌握到什么程度

写业务代码时,你可能天天都在和 IO 打交道:读取文件调用 HTTP 接口操作数据库读写 Redis 。但有没有想过:这些 IO 操作底层是怎么工作的为什么文件读取会阻塞NIO 到底好在哪为什么 Netty 用 NIO 而不用 BIO

很多同学觉得 IO 是「底层」东西,交给框架就行了。但理解 IO 模型,能帮你:

  • 选型时知道什么时候该用同步、什么时候该用异步
  • 排查问题时知道瓶颈在 CPU、内存、网络还是 IO
  • 理解 Netty、Redis、MQ 等中间件的底层原理

这篇帮你把 IO/NIO 彻底搞明白。下面我按「IO 基础 → IO 模型演进 → NIO 核心组件 → 实战场景」的顺序往下聊。


1. IO 基础:什么是输入输出?📡

1.1 什么是 IO?

IO(Input/Output) 就是数据的输入输出。在计算机里,一切 IO 都是数据的流动:

text 复制代码
数据流动方向:
  │
  ├─ 输入(Input):数据从外部设备流向程序
  │   ├─ 读取文件
  │   ├─ 接收网络数据
  │   └─ 读取键盘输入
  │
  └─ 输出(Output):数据从程序流向外部设备
      ├─ 写入文件
      ├─ 发送网络数据
      └─ 屏幕输出

1.2 常见的 IO 设备

设备 方向 特点
磁盘 读写 相对较慢,顺序读写快
网络 收发 受网络延迟影响
内存 读写 极快
键盘/屏幕 输入/输出 交互用

1.3 Java 中的 IO 分类

java 复制代码
// 字节流
InputStream / OutputStream

// 字符流(带编码转换)
Reader / Writer

// 节点流(直接操作数据源)
FileInputStream / FileOutputStream
SocketInputStream / SocketOutputStream

// 处理流(包装其他流)
BufferedInputStream / BufferedReader
DataInputStream / DataOutputStream
ObjectInputStream / ObjectOutputStream

2. IO 模型演进:从阻塞到异步 🚀

2.1 阻塞 IO(BIO,Blocking IO)

最传统的方式 :发起 IO 请求后,线程一直等待,直到数据就绪或操作完成。

java 复制代码
// BIO 读取文件
FileInputStream fis = new FileInputStream("file.txt");
byte[] buffer = new byte[1024];
int len = fis.read(buffer);  // 阻塞!等待数据就绪
fis.close();

BIO 的问题

  • 线程在等待时什么都不干,浪费 CPU
  • 每个连接一个线程,高并发下线程资源耗尽
text 复制代码
BIO 模型:
  客户端 1 → ─┐
  客户端 2 → ─┼─→ 线程池 → 阻塞等待 → 读取数据
  客户端 3 → ─┘
  
问题:线程数 = 连接数,连接多了线程不够用

2.2 非阻塞 IO(NIO,Non-blocking IO)

发起请求后立即返回,如果数据未就绪,返回「数据未就绪」状态,线程可以继续做其他事。

java 复制代码
// NIO 读取文件(伪代码)
FileChannel channel = FileChannel.open(path);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);  // 非阻塞,立即返回
// 轮询检查数据是否就绪
while (buffer.hasRemaining()) {
    // 处理数据
}

NIO 的优势

  • 少量线程可以处理大量连接
  • 线程不会被阻塞,可以做其他事
text 复制代码
NIO 模型:
  客户端 1 → ─┐
  客户端 2 → ─┼─→ Selector(单线程)→ 就绪事件通知
  客户端 3 → ─┘
  
优势:一个线程管理多个连接

2.3 IO 多路复用(Multiplexing)

一个线程管理多个 IO 通道,通过 Selector 监听多个通道的就绪状态。

java 复制代码
// 多路复用示例
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();  // 阻塞,直到有事件就绪
    Set<SelectionKey> keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        if (key.isAcceptable()) {
            // 处理新连接
        } else if (key.isReadable()) {
            // 处理读事件
        }
    }
}

多路复用的优势

  • 单线程管理大量连接
  • Linux 下用 epoll,效率极高

2.4 异步 IO(AIO,Asynchronous IO)

完全异步:发起 IO 请求后,操作系统完成 IO 操作后通知应用程序。

java 复制代码
// AIO 示例(Java 7+)
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // IO 完成后自动调用
    }
    
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        // 失败时调用
    }
});

2.5 IO 模型对比

模型 阻塞 线程数 复杂度 适用场景
BIO 1 连接 1 线程 简单 连接数少、低并发
NIO 1 线程 N 连接 中等 高并发连接
多路复用 1 线程 N 连接 中等 高并发连接(主流)
AIO 事件驱动 复杂 极致性能

3. NIO 核心组件 🔧

3.1 Buffer(缓冲区)

NIO 的核心是 Buffer,数据从 Channel 读写到 Buffer。

java 复制代码
// 创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);  // 堆内存
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);  // 直接内存(零拷贝)

// 写入数据
buffer.put("hello".getBytes());

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

// 读取数据
while (buffer.hasRemaining()) {
    System.out.print((char) buffer.get());
}

// 清空缓冲区
buffer.clear();

Buffer 的三个状态

text 复制代码
Buffer 状态:
  │
  ├─ 写模式(flip 前):
  │   position → 写入位置
  │   limit → 容量
  │   capacity → 容量
  │
  ├─ 读模式(flip 后):
  │   position → 读取位置
  │   limit → 数据结束位置
  │   capacity → 容量
  │
  └─ 清空(clear 后):
      position → 0
      limit → 容量
      capacity → 容量

3.2 Channel(通道)

Channel 类似于 Stream,但可以非阻塞读写。

java 复制代码
// 文件 Channel
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);

// Socket Channel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));

// Server Socket Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));

3.3 Selector(选择器)

Selector 监控多个 Channel 的状态,实现多路复用。

java 复制代码
// 创建 Selector
Selector selector = Selector.open();

// 注册 Channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);  // 非阻塞
serverChannel.register(selector, SelectionKey.OP_ACCEPT);

// 事件循环
while (true) {
    selector.select();  // 阻塞,等待事件
    
    for (SelectionKey key : selector.selectedKeys()) {
        if (key.isAcceptable()) {
            // 新连接
        } else if (key.isReadable()) {
            // 可读
        } else if (key.isWritable()) {
            // 可写
        }
    }
}

3.4 直接内存 vs 堆内存

java 复制代码
// 堆内存 Buffer(GC 管理)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// 直接内存 Buffer(操作系统内存,零拷贝)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

直接内存的优势

  • 不经过 JVM 内存,直接和操作系统交互
  • 减少一次内存拷贝(零拷贝)
  • 适合大文件传输、网络通信

直接内存的劣势

  • 分配/释放成本高
  • 不受 JVM 管理,需要手动释放

4. 常见 IO 场景实战 🎯

4.1 文件读取

java 复制代码
// BIO 方式
try (BufferedReader reader = new BufferedReader(
        new FileReader("file.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

// NIO 方式
try (FileChannel channel = FileChannel.open(
        Paths.get("file.txt"), StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (channel.read(buffer) != -1) {
        buffer.flip();
        System.out.println(StandardCharsets.UTF_8.decode(buffer));
        buffer.clear();
    }
}

4.2 文件写入

java 复制代码
// BIO 方式
try (BufferedWriter writer = new BufferedWriter(
        new FileWriter("output.txt"))) {
    writer.write("Hello World");
}

// NIO 方式
try (FileChannel channel = FileChannel.open(
        Paths.get("output.txt"), 
        StandardOpenOption.CREATE, 
        StandardOpenOption.WRITE)) {
    ByteBuffer buffer = StandardCharsets.UTF_8.encode("Hello World");
    channel.write(buffer);
}

4.3 网络通信

java 复制代码
// NIO Server
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);

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

while (true) {
    selector.select();
    for (SelectionKey key : selector.selectedKeys()) {
        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);
            client.read(buffer);
            // 处理数据
        }
    }
}

4.4 大文件传输(零拷贝)

java 复制代码
// 普通方式:需要两次拷贝
// 用户空间 → 内核缓冲区 → 磁盘
FileInputStream fis = new FileInputStream("file.txt");
OutputStream os = new FileOutputStream("output.txt");
byte[] buffer = new byte[8192];
int len;
while ((len = fis.read(buffer)) != -1) {
    os.write(buffer, 0, len);
}

// 零拷贝方式:一次拷贝
// 内核缓冲区 → 磁盘(直接 DMA)
FileChannel inChannel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("output.txt"), 
    StandardOpenOption.CREATE, StandardOpenOption.WRITE);
inChannel.transferTo(0, inChannel.size(), outChannel);

5. Spring Boot 中的 IO 操作 📦

5.1 文件上传

java 复制代码
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) throws IOException {
    // 上传到本地
    String path = "/uploads/" + file.getOriginalFilename();
    file.transferTo(Paths.get(path));
    
    // 或者用流
    file.getInputStream();  // 获取输入流
    
    return Result.success(path);
}

5.2 文件下载

java 复制代码
@GetMapping("/download")
public ResponseEntity<Resource> download(String filename) throws IOException {
    Path path = Paths.get("/files/" + filename);
    Resource resource = new UrlResource(path.toUri());
    
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, 
            "attachment; filename=\"" + filename + "\"")
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .body(resource);
}

5.3 HTTP 请求(RestTemplate)

java 复制代码
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}

// 使用
String result = restTemplate.getForObject(
    "http://localhost:8080/api/data", 
    String.class);

5.4 HTTP 请求(WebClient)

java 复制代码
WebClient client = WebClient.create("http://localhost:8080");

Mono<String> result = client.get()
    .uri("/api/data")
    .retrieve()
    .bodyToMono(String.class);

6. IO 性能优化建议 ⚡

6.1 减少 IO 次数

java 复制代码
// ❌ 频繁 IO
for (String line : lines) {
    writer.write(line);  // 每次都 IO
}

// ✅ 批量 IO
StringBuilder sb = new StringBuilder();
for (String line : lines) {
    sb.append(line).append("\n");
}
writer.write(sb.toString());  // 一次 IO

6.2 使用缓冲流

java 复制代码
// ❌ 无缓冲
FileInputStream fis = new FileInputStream("file.txt");

// ✅ 有缓冲
BufferedInputStream bis = new BufferedInputStream(
    new FileInputStream("file.txt"), 8192);

6.3 使用直接内存

java 复制代码
// 大文件传输用直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);

6.4 异步 IO

java 复制代码
// 同步阻塞 → 异步非阻塞
CompletableFuture.supplyAsync(() -> {
    // 在线程池中执行 IO
    return restTemplate.getForObject(url, String.class);
});

7. 常见面试题 📝

7.1 BIO 和 NIO 的区别?

  • BIO:阻塞 IO,一个线程处理一个连接
  • NIO:非阻塞 IO,一个线程处理多个连接(多路复用)

7.2 什么是零拷贝?

传统方式:用户空间 → 内核缓冲区 → 磁盘(两次拷贝)

零拷贝:内核缓冲区 → 磁盘(一次拷贝),通过 transferTo() 实现

7.3 Buffer 的 flip() 作用?

flip() 将 Buffer 从写模式切换到读模式,同时重置 position 到 0,limit 到数据结束位置。

7.4 Selector 的工作原理?

Selector 监听多个 Channel 的就绪状态(可读、可写、可连接),当有事件就绪时返回,应用程序可以处理对应的事件。

7.5 为什么 Netty 用 NIO?

  • 高性能:一个线程管理大量连接
  • 低资源:线程数不随连接数增长
  • 零拷贝:支持直接内存

小结

  • IO 是数据的输入输出,常见有磁盘 IO、网络 IO
  • BIO 阻塞 IO,一个线程处理一个连接,高并发下性能差
  • NIO 非阻塞 IO,通过 Buffer/Channel/Selector 实现多路复用
  • 直接内存适合大文件传输和网络通信,减少内存拷贝
  • 零拷贝 通过 transferTo() 实现,减少一次内核拷贝
  • Spring Boot 中文件上传用 MultipartFile,HTTP 请求用 RestTemplateWebClient

下一篇(020)预告Optional、Stream、Lambda:风格与性能注意点------函数式编程、Stream 惰性求值、性能陷阱。

相关推荐
Nicander2 小时前
JDBC PreparedStatement的作用机制
java
MegaDataFlowers2 小时前
解决idea报错不支持发行版本21
java·ide·intellij-idea
DevilSeagull2 小时前
MySQL(1) 安装与配置
java·数据库·git·mysql·http·开源·github
季明洵2 小时前
Java基础---逻辑控制(上)
java·开发语言·循环结构·分支结构·顺序结构
Cyan_RA92 小时前
如何利用 Paddle-OCR 丝滑进行复杂版面 PDF 的批量化OCR处理?
java·linux·python·ocr·conda·paddle·surya
程序员清风2 小时前
2026年AI编程工具对比:谁最值得用?
java·后端·面试
希望永不加班2 小时前
SpringBoot 多级缓存(本地缓存 + Redis)
java·spring boot·redis·后端·缓存
沫璃染墨2 小时前
重生之我要手写 C++ list:从底层结构到 const 迭代器与迭代器失效全解
开发语言·c++
C雨后彩虹2 小时前
文件目录大小
java·数据结构·算法·华为·面试