第36篇:选择最适合自己的NIO,一探流技术

📌 系列导航《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第35篇:Java异常处理最佳实践 |
➡️ 下一篇:第37篇:Java内部类及其作用


一、核心知识点

  • BIO(Blocking I/O)、NIO(Non-blocking I/O)、AIO(Asynchronous I/O)的区别
  • 传统流体系:字节流、字符流、缓冲流、转换流
  • NIO 核心组件:Channel(通道)、Buffer(缓冲区)、Selector(多路复用器)
  • 文件操作与网络操作示例
  • 压缩流(GZIP)等

二、通俗讲解(1分钟开心学)

1. I/O 模型的演进

  • BIO(同步阻塞):一个连接一个线程,线程在读写数据时会阻塞。适合连接数少、处理快的场景。
  • NIO(同步非阻塞):一个线程可以管理多个连接(Selector),当数据准备好时才处理。适合连接数多但每个连接不频繁通信的场景(如聊天服务器)。
  • AIO(异步非阻塞):数据准备好后回调通知,不需要主动轮询。适合高并发、长连接的场景(Java 7 引入,但实际使用不如 NIO 普遍)。

2. 传统流(BIO)体系

  • 字节流InputStream / OutputStream,处理二进制数据(图片、音视频)。
  • 字符流Reader / Writer,处理文本,自动处理字符编码。
  • 缓冲流BufferedInputStream / BufferedReader,减少系统调用,提高性能。
  • 转换流InputStreamReader / OutputStreamWriter,字节与字符之间的桥梁。

3. NIO 三大核心

  • Channel:双向通道,可读可写(类似流但功能更强)。
  • Buffer:缓冲区,数据容器(如 ByteBuffer)。
  • Selector:多路复用器,监听多个 Channel 的事件(读就绪、写就绪等)。

4. 为什么 NIO 性能更好?

BIO 每个连接占用一个线程,线程上下文切换开销大。NIO 通过 Selector 复用线程,只有数据准备好时才处理,资源占用低。

生活类比
BIO 就像餐厅里每个顾客配一个服务员,服务员一直守着,顾客不说话也站着。
NIO 就像一个服务员同时服务多个顾客,哪个顾客举手(数据就绪)就去哪个,其他时间可以干别的。

三、实操代码案例 + 场景说明

场景:文件复制,分别用 BIO 流和 NIO 的 FileChannel 实现,并对比效率。

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class NIODemo {
    // BIO 文件复制
    public static void copyWithBIO(String src, String dest) throws IOException {
        try (InputStream in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dest)) {
            byte[] buffer = new byte[8192];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
    }
    
    // NIO 文件复制(使用 FileChannel)
    public static void copyWithNIO(String src, String dest) throws IOException {
        try (FileChannel srcChannel = FileChannel.open(Paths.get(src), StandardOpenOption.READ);
             FileChannel destChannel = FileChannel.open(Paths.get(dest), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            srcChannel.transferTo(0, srcChannel.size(), destChannel);
        }
    }
    
    // NIO 带缓冲区的手动复制
    public static void copyWithNIOBuffer(String src, String dest) throws IOException {
        try (FileChannel srcChannel = new FileInputStream(src).getChannel();
             FileChannel destChannel = new FileOutputStream(dest).getChannel()) {
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            while (srcChannel.read(buffer) != -1) {
                buffer.flip();
                destChannel.write(buffer);
                buffer.clear();
            }
        }
    }
    
    // GZIP 压缩(BIO 流的典型应用)
    public static void compressGZIP(String src, String dest) throws IOException {
        try (InputStream in = new FileInputStream(src);
             OutputStream out = new GZIPOutputStream(new FileOutputStream(dest))) {
            byte[] buf = new byte[8192];
            int len;
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        String src = "largefile.dat";
        String dest = "copy.dat";
        
        long start = System.nanoTime();
        copyWithBIO(src, dest);
        System.out.println("BIO time: " + (System.nanoTime() - start) / 1e6 + " ms");
        
        start = System.nanoTime();
        copyWithNIO(src, dest);
        System.out.println("NIO transferTo: " + (System.nanoTime() - start) / 1e6 + " ms");
    }
}

NIO 服务器简例(理解 Selector)

// 核心代码示意
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.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()) { /* 读数据 */ }
    }
    keys.clear();
}

四、避坑要点

错误/误区 后果 正确做法
NIO 中忘记 flip() 读不到数据或读到错误数据 写后读前调用 flip()
直接缓冲区频繁分配 性能下降 复用缓冲区,使用对象池
Selector 处理完事件不移除 无限循环 用完后 iterator.remove()
混淆字符流和字节流 乱码或数据损坏 文本用字符流,二进制用字节流

五、面试高频考点

Q1:BIO、NIO、AIO 的区别?

BIO 阻塞,一连接一线程;NIO 同步非阻塞,通过 Selector 管理多个连接;AIO 异步非阻塞,基于回调。NIO 适合高并发低流量(如聊天),AIO 适合高并发长连接(但实现复杂)。

Q2:NIO 的核心组件有哪些?

Channel(双向通道)、Buffer(数据容器)、Selector(事件多路复用器)。

Q3:什么是零拷贝?

FileChannel.transferTo() 可以直接将数据从文件系统缓存传输到网络套接字缓冲区,绕过用户态内存拷贝,提高大文件传输效率。

六、练习题

  1. 对比:分别用 BIO 和 NIO 实现文件复制,比较耗时。
  2. 编程:使用 NIO 的 FileChannel 实现一个方法,将文件内容全部读取到 ByteBuffer 中并打印。
  3. 简答:为什么 NIO 适合高并发场景?

📊 你的学习进度

  • 当前:第36篇 / 共44篇 · 第六阶段:NIO、泛型、JVM内幕、字节码(第36~44篇)
  • ✅ 已完成:第1~35篇
  • 📖 正在学:第36篇
  • ⏳ 待学习:第37~44篇

👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇

💡 本专栏每篇都包含:避坑表 + 面试高频考点 + 练习题。每天30分钟,100天拿offer!


👉 下一篇文章预告

《第37篇:Java内部类及其作用》

内容简介:成员内部类、静态内部类、局部内部类、匿名内部类,访问规则,内存泄漏风险。

💡 学完这篇,你将掌握内部类的所有用法,写出更优雅的代码结构。

📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!
📌 除了Java,我也在深挖智能物流实战(出版社WMS、托盘调度、机器学习落地)。如果你对技术在不同领域的实战感兴趣,欢迎点击我的头像,看看专栏《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐