Java I/O 模型详解
前言
Java 中有 3 种主要的 I/O 模型:同步阻塞 I/O(BIO)、同步非阻塞 I/O(NIO)和异步非阻塞 I/O(AIO),除了这 3 种主要的 I/O 模型,还有多路复用 I/O 模型和信号驱动模型。它们的区别主要在于处理 I/O 操作时线程的行为方式,以及应用程序对于 I/O 完成时的处理方式。
I/O 模型介绍
I/O 模型的简介
同步阻塞 I/O 模型(Blocking I/O,简称阻塞 I/O)是 Java 最早引入的模型之一,它的特点是在执行 I/O 操作时会阻塞当前线程,直到 I/O 操作完成才会继续执行后续代码。在同步阻塞 I/O 模型中,当一个线程调用读取操作时,如果没有数据可读,线程将一直阻塞在读取操作上,直到有数据到达为止。同样,当一个线程调用写入操作时,如果写缓冲区已满,线程将一直阻塞在写入操作上,直到有空间可用为止。同步阻塞 I/O 模型的优点是简单易用,但其缺点是效率较低,不适用高并发场景,因为线程在等待 I/O 操作完成时会被阻塞,无法处理其他任务。
同步非阻塞 I/O 模型(Non-blocking I/O,简称非阻塞 I/O)是对同步阻塞 I/O 模型(BIO)的改进,从 Java 1.4 开始支持。在同步非阻塞 I/O 模型中,当一个线程调用读取操作时,如果没有数据可读,线程不会被阻塞,而是立即返回一个错误码或空值。同样,当一个线程调用写入操作时,如果写缓冲区已满,线程也不会被阻塞,而是立即返回一个错误码。通过不断地轮询 I/O 操作的状态,同步非阻塞 I/O 模型可以实现在等待 I/O 操作完成的同时处理其他任务。同步非阻塞 I/O 模型的优点是能够提高系统的并发性能,但其缺点是需要频繁地轮询 I/O 操作的状态,会造成 CPU 资源的浪费,而且实现相对复杂,需要一定的编程技巧。这种模型适用于需要处理多个连接但每个连接比较短(轻操作)的场景,如实时通讯系统、聊天服务器等。
多路复用 I/O 模型(Multiplexing I/O)可以实现同时监控多个 I/O 操作的状态。Java 中的多路复用 I/O 一般是建立在同步非阻塞 I/O 模型(NIO)基础之上实现的,比如 Netty 网络编程框架。在多路复用 I/O 模型中,一个线程可以同时监听多个 I/O 操作的状态,当某个 I/O 操作就绪时,线程可以进行相应的读取或写入操作。通过这种方式,多路复用 I/O 模型可以在一个线程中处理多个 I/O 操作,提高系统的并发性能。多路复用 I/O 模型的优点是能够有效地减少线程的数量,降低系统资源的消耗,但其缺点是实现复杂度较高,需要一定的编程技巧。
异步非阻塞 I/O 模型(Asynchronous I/O,简称异步 I/O)是最高级别的 I/O 模型之一,性能和吞吐量最高,从 Java 1.7 开始支持。它通过将 I/O 操作的结果通知给应用程序,来实现非阻塞的 I/O 操作。在异步非阻塞 I/O 模型中,应用程序发起一个 I/O 操作后,不需要等待操作完成,而是可以继续执行其他任务。当 I/O 操作完成后,操作系统会通知应用程序,应用程序再进行相应的处理。异步非阻塞 I/O 模型的优点是能够充分利用系统资源,提高系统的并发性能,但其缺点是需要操作系统的支持,在某些操作系统(如 Windows)上的支持不如 NIO 成熟,对于编程人员来说,实现相对复杂。这种模型适用于需要处理多个连接且每个连接比较长(重操作),并且要求高性能、高并发的场景,例如高性能服务器、流媒体服务器等。
上面的同步、异步是针对请求(如 HTTP 请求)而言的,而阻塞和非阻塞是针对客户端(如浏览器)而言的。比如,在一次网络请求中,客户端会发送一个请求到服务器:
- 客户端发送请求后,就一直等待服务端响应。此时,客户端:阻塞,请求:同步。
- 客户端发送请求后,就去干别的事情了,时不时过来检查一下服务端是否返回响应。此时,客户端:非阻塞,请求:同步。
- 客户端发送请求后,就去干别的事情了,等到服务端返回响应后,再过来处理结果。此时,客户端:非阻塞,请求:异步。
I/O 模型的区别
同步阻塞 I/O(Blocking I/O,简称阻塞 I/O):
- 在同步阻塞 I/O 模型中,当一个线程执行一个 I/O 操作时,它会一直阻塞直到该操作完成。这意味着线程会一直等待,直到数据可读取或者可以写入。
- 同步阻塞 I/O 模型通常会导致线程资源的浪费,因为线程可能会长时间地等待 I/O 操作完成,而此时它无法执行其他任务。
- 优点:简单易用,在连接数不多且并发要求不高的情况下,性能表现可以接受。
- 缺点:每个连接都需要一个独立的线程,当连接数较多时,性能和吞吐量下降明显,并容易造成线程资源浪费。
- 适用场景:适用于连接数较少且并发要求不高的场景,例如传统的 Web 应用。
同步非阻塞 I/O(Non-blocking I/O,简称非阻塞 I/O):
- 在同步非阻塞 I/O 模型中,当一个线程执行一个 I/O 操作时,它不会被阻塞。线程会立即返回,告诉调用者当前操作无法完成,而不是一直等待。
- 如果 I/O 操作没有完成,应用程序可以继续做其他事情,而不必等待该操作完成。
- 优点:单线程能够管理多个连接,提高了并发处理能力,减少了线程资源的浪费。
- 缺点:需要频繁地轮询 I/O 操作的状态,容易造成 CPU 资源的浪费。
- 适用场景:适用于需要处理多个连接但每个连接比较短(轻操作)的场景,如实时通讯系统、聊天服务器等。
异步非阻塞 I/O(Asynchronous I/O,简称异步 I/O):
- 在异步非阻塞 I/O 模型中,一个 I/O 操作的完成不会导致调用线程阻塞。相反,应用程序会提交一个 I/O 请求,然后继续执行其他任务。
- 当 I/O 操作完成时,系统会通知应用程序,或者调用预先指定的回调函数来处理完成事件。
- 优点:相比于 NIO 而言,AIO 更加灵活,能够在 I/O 操作完成时异步地通知应用程序。
- 缺点:需要操作系统的支持,在某些操作系统(如 Windows)上的支持不如 NIO 成熟,对于编程人员来说,实现相对复杂。
- 适用场景:适用于需要处理多个连接且每个连接比较长(重操作),并且要求高性能、高并发的场景,例如高性能服务器、流媒体服务器等。
不同 I/O 模型之间的区别总结:
- 同步阻塞 I/O 会导致线程阻塞,而同步非阻塞 I/O 和异步非阻塞 I/O 允许线程在 I/O 操作完成期间继续执行其他任务。
- 同步非阻塞 I/O 允许一个线程处理多个 I/O 通道,而异步非阻塞 I/O 更加灵活,可以在 I/O 操作完成后异步处理结果。
- 异步非阻塞 I/O 提供了最大的性能和资源利用率,但是它的实现相对复杂,需要一定的编程技巧。
同步非阻塞 I/O 模型
核心组件的介绍
Java NIO 的核心组件分别是 Buffer(缓冲区)、Channel(通道)、Selector(选择器),它们的关系如下图所示。Channel 类似于流,每个 Channel 对应一个 Buffer,而且 Channel 会注册到 Selector 中去。Selector 会根据 Channel 上发生的读 / 写事件,将请求交给某个空闲线程进行处理。值得一提的是,每个 Selector 对应一个线程,Buffer 和 Channel 都是可读可写的。
Buffer(缓冲区):
- 缓冲区是 NIO 中的基本数据容器。它是一块连续的内存块,用于存储数据。在 NIO 中,所有数据都必须首先存入缓冲区,然后才能从缓冲区读取。
- 主要子类有 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer,分别用于不同类型的数据。
Channel(通道):
- 通道是 NIO 中用于进行数据传输的对象。它可以与缓冲区进行交互,将数据从缓冲区读入通道,或者将数据从通道写入缓冲区。
- Java NIO 提供了多种类型的通道,如 FileChannel、SocketChannel、ServerSocketChannel 和 DatagramChannel,用于处理文件、网络等不同的 I/O 操作。
Selector(选择器):
- 选择器是 NIO 中用于多路复用的关键组件。它允许单个线程同时监听多个通道的 I/O 事件,当某个通道有数据可读或者可写事件时,可以立即处理该事件。
- 使用选择器可以减少线程的数量,提高系统的并发处理能力,使得在单个线程中可以同时处理多个通道的 I/O 操作。