杰瑞科技汇

java io 和nio 区别

核心思想:一句话总结

  • BIO (Blocking I/O)同步阻塞,一个线程发起一个 I/O 操作(如读或写),如果数据还没准备好,这个线程就必须阻塞等待,直到数据准备好并完成 I/O 操作。
  • NIO (New I/O)同步非阻塞,一个线程发起一个 I/O 操作,如果数据还没准备好,这个线程不会阻塞,而是可以去干别的事情,它会定期去“轮询”数据是否准备好,当数据准备好后,线程再去进行 I/O 操作,这个操作本身是阻塞的。

核心概念与模型对比

特性 BIO (传统 I/O) NIO (New I/O)
核心模型 一个连接一个线程 一个线程处理多个连接
I/O 模型 同步阻塞 同步非阻塞
通信方式 缓冲区
多路复用 有 (通过 Selector)
线程模型 每个 I/O 操作都需要一个独立的线程来处理。 一个线程可以管理多个 Channel,通过 Selector 轮询所有 Channel 的 I/O 事件。

详细对比分析

I/O 模型:阻塞 vs. 非阻塞

这是两者最根本的区别。

java io 和nio 区别-图1
(图片来源网络,侵删)
  • BIO (阻塞)

    • 工作方式:当一个线程调用 InputStream.read()OutputStream.write() 时,该线程会被挂起(阻塞),直到数据从网络或磁盘读取到缓冲区,或者数据从缓冲区写入成功。
    • 缺点:在等待 I/O 的这段时间里,CPU 资源被浪费了,线程什么也做不了,如果连接数很多,就需要创建大量的线程,每个线程都可能在阻塞,这会急剧消耗系统资源(内存、CPU 上下文切换),导致性能急剧下降,甚至系统崩溃,这就是所谓的 C10K 问题(如何同时处理 1 万个客户端连接)。
    // BIO 伪代码
    Socket socket = serverSocket.accept(); // 阻塞,等待客户端连接
    InputStream in = socket.getInputStream();
    byte[] buffer = new byte[1024];
    int len = in.read(buffer); // 阻塞,等待数据读取
    // ... 处理数据

    在服务器端,通常会为每个客户端连接创建一个新的线程来处理,像这样:

    while (true) {
        Socket clientSocket = serverSocket.accept(); // 阻塞
        new Thread(() -> {
            handleClient(clientSocket); // 在新线程中处理
        }).start();
    }
  • NIO (非阻塞)

    • 工作方式:线程发起一个 I/O 操作(如 channel.read(buffer)),如果数据还没准备好,这个方法会立即返回一个 0 或表示“尚未准备好”的值,线程不会被阻塞,线程可以去执行其他任务。
    • 轮询机制:线程需要不断地去“询问” Channel 数据是否准备好,这个“询问”的过程就是 轮询,当轮询到某个 Channel 的数据准备好了,线程再去执行真正的读写操作,这个读写操作本身是阻塞的,但由于准备阶段是非阻塞的,整体效率大大提高。
    • 优点:一个线程可以同时处理多个 Channel 的 I/O 操作,极大地减少了线程数量,降低了系统开销,提高了系统的吞吐量和并发能力。
    // NIO 伪代码
    Selector selector = Selector.open();
    channel.configureBlocking(false); // 设置为非阻塞模式
    channel.register(selector, SelectionKey.OP_READ); // 注册感兴趣的事件
    while (true) {
        selector.select(); // 阻塞,等待至少一个 Channel 的事件就绪
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        Iterator<SelectionKey> iter = selectedKeys.iterator();
        while (iter.hasNext()) {
            SelectionKey key = iter.next();
            if (key.isReadable()) {
                SocketChannel channel = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int len = channel.read(buffer); // 如果数据未准备好,会立即返回 0
                if (len > 0) {
                    // ... 处理数据
                }
            }
            iter.remove();
        }
    }

通信单位:流 vs. 缓冲区

  • BIO (流)

    java io 和nio 区别-图2
    (图片来源网络,侵删)
    • 特点:I/O 是面向的,你可以想象一个水管,数据像水流一样,只能从一端流向另一端,它没有缓冲区,不能随意地前后移动数据,每次读取都是从流的当前位置开始。
    • APIInputStream, OutputStream
  • NIO (缓冲区)

    • 特点:I/O 是面向缓冲区的,数据被读入一个 ByteBuffer 对象中,缓冲区是一个可以双向移动的内存块,你可以像操作数组一样操作它,可以设置 position 和 limit 来控制读写位置,这使得数据操作更加灵活和高效。
    • APIByteBuffer, CharBuffer 等。

多路复用:无 vs. Selector

这是 NIO 能够实现“一个线程处理多个连接”的关键。

  • BIO (无)

    每个 Socket 连接都需要一个独立的线程来管理和监听,无法复用,线程和连接是 1:1 的关系。

  • NIO (Selector)

    • Selector(选择器)是 NIO 的核心,它像一个“多路开关”,可以同时监听多个 Channel 的 I/O 事件(如连接、读、写)。
    • 工作流程
      1. 将多个非阻塞的 Channel 注册到同一个 Selector 上,并告诉 Selector 我们关心哪些事件(如 SelectionKey.OP_READ)。
      2. 调用 Selector.select() 方法,这个方法会阻塞,直到至少一个注册的 Channel 产生了我们关心的事件。
      3. select() 返回后,我们可以通过 Selector.selectedKeys() 获取所有发生了事件的 SelectionKey 集合。
      4. 遍历这些 SelectionKey,对每个事件进行处理(如从对应的 Channel 读取数据)。
    • 效果:通过 Selector,一个线程就可以高效地管理成百上千个连接,完美解决了 BIO 的线程瓶颈问题。

一个生动的比喻

  • BIO (餐厅服务)

    • 一个服务员(线程)只服务一桌客人(连接)。
    • 当客人点菜后,服务员就去厨房等,直到菜做好(阻塞),然后端回来,这期间,这桌客人占用着这位服务员,不能服务其他客人。
    • 如果餐厅有 100 桌客人,就需要 100 个服务员,成本极高。
  • NIO (餐厅服务)

    • 一个服务员(线程)服务所有客人(连接)。
    • 服务员会巡视所有桌子(轮询 Selector)。
    • A 桌客人点菜了,服务员就记下“A桌要等菜”,然后去 B 桌问需求。
    • 服务员不断巡视,直到发现 A 桌的菜好了(事件就绪),就去端菜。
    • 这样,一个服务员就能高效地管理所有桌子,大大提高了服务效率。

优缺点总结

BIO (传统 I/O) NIO (New I/O)
优点 模型简单,易于理解和实现。
同步阻塞模型,流程清晰,代码逻辑直接。
并发性能高,一个线程可处理多个连接。
系统资源消耗低,线程数量少。
适合高并发场景,能有效应对 C10K 问题。
缺点 并发能力差,连接数多时,线程数激增。
资源消耗大,线程创建和销毁、上下文切换成本高。
可扩展性差,无法应对大规模并发连接。
模型复杂,编程难度大,需要理解 Selector, Channel, Buffer 等概念。
代码量多,实现一个 NIO 服务器比 BIO 复杂得多。
Bug 难以排查,多路复用的逻辑增加了调试难度。
对 Buffer 操作要求高,需要手动管理 position 和 limit。

适用场景

  • 选择 BIO 的场景

    • 连接数少且固定的应用,与硬件设备、数据库的连接。
    • 对性能要求不高,开发速度优先的场景。
    • 简单的命令行工具或桌面应用。
  • 选择 NIO 的场景

    • 高并发的网络服务器,Web 服务器、RPC 框架、聊天室、游戏服务器等。
    • 需要同时处理成千上万个客户端连接的应用。
    • 对性能和资源利用率有极高要求的应用。

演进:AIO (Asynchronous I/O)

为了进一步简化编程并提升性能,Java 7 引入了 AIO(Asynchronous I/O),也叫 NIO.2。

  • 模型异步非阻塞
  • 核心思想:当你发起一个 I/O 操作后,你可以立即去做别的事情,当 I/O 操作完成时,系统会通过回调Future机制通知你。
  • 对比 NIO:NIO 是“我主动去问数据好了没”(同步非阻塞),AIO 是“数据好了你告诉我”(异步非阻塞),AIO 的编程模型更接近人的思维,代码更简洁,但底层实现更复杂。
  • 适用场景:AIO 主要用于I/O 密集型且延迟敏感的场景,如文件操作和高速网络通信,在 Linux 系统上,AIO 的实现依赖于 epoll,其性能在某些场景下可能不如 NIO,NIO 依然是构建高性能网络服务的主流选择。
BIO NIO AIO
I/O 模型 同步阻塞 同步非阻塞 异步非阻塞
核心 一个连接一个线程 一个线程多路复用 回调机制
编程难度 简单 复杂 中等(概念上简单,实现复杂)
性能 高,但依赖具体实现和场景

对于 Java 深刻理解 BIO 和 NIO 的区别是迈向高性能编程的必经之路,在开发网络应用时,应根据业务场景的并发需求来选择合适的 I/O 模型。

分享:
扫描分享到社交APP
上一篇
下一篇