# IO与流

IO，即in和out，也就是输入和输出，指应用程序和外部设备之间的数据传递，常见的外部设备包括**文件、管道、网络**连接。

Java 中是通过流处理IO 的，那么什么是流？

流（Stream），是一个抽象的概念，是指一连串的数据（字符或字节），是以先进先出的方式发送信息的通道。

当程序需要读取数据的时候，就会开启一个通向数据源的流，这个数据源可以是文件，内存，或是网络连接。类似的，当程序需要写入数据的时候，就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。

一般来说关于流的特性有下面几点：

* 先进先出：最先写入输出流的数据最先被输入流读取到。
* 顺序存取：可以一个接一个地往流中写入一串字节，读出时也将按写入顺序读取一串字节，不能随机访问中间的数据。（RandomAccessFile除外）
* 只读或只写：每个流只能是输入流或输出流的一种，不能同时具备两个功能，输入流只能进行读操作，对输出流只能进行写操作。在一个数据传输通道中，如果既要写入数据，又要读取数据，则要分别提供两个流。

## 传输方式分类

传输方式有两种，字节和字符

* 字节（byte）是计算机中用来表示存储容量的一个计量单位，通常情况下，一个字节有 8 位（bit）。用来处理二进制文件（等一切文件），比如说图片、MP3 、视频。字节流本身没有缓冲区，缓冲字节流相对于字节流，效率提升非常高。由于一个字符通常由多个字节组成，如果用字节流读中文，读出来的是乱码。
* 字符（char）可以是计算机中使用的字母、数字、和符号，比如说 A 1 $ 这些。通常来说，一个字母或者一个字符占用一个字节，一个汉字占用两个字节。用来处理文本文件。字符流本身就带有缓冲区。**字符流 = 字节流 + 编码表**

![](https://1780240882-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiUkJxV19BIulXoLHTeDI%2Fuploads%2Fgit-blob-11836af67469b3215d4270512a821c704a234cc1%2Fshangtou-01.png?alt=media)

虽然 IO 类很多，但核心的就是 4 个抽象类：InputStream、OutputStream、Reader、Writer。核心方法也就 2 个：read 和 write。

**InputStream 类**

* `int read()`：读取数据
* `int read(byte b[], int off, int len)`：从第 off 位置开始读，读取 len 长度的字节，然后放入数组 b 中
* `long skip(long n)`：跳过指定个数的字节
* `int available()`：返回可读的字节数
* `void close()`：关闭流，释放资源

**OutputStream 类**

* `void write(int b)`： 写入一个字节，虽然参数是一个 int 类型，但只有低 8 位才会写入，高 24 位会舍弃（这块后面再讲）
* `void write(byte b[], int off, int len)`： 将数组 b 中的从 off 位置开始，长度为 len 的字节写入
* `void flush()`： 强制刷新，将缓冲区的数据写入
* `void close()`：关闭流

**Reader 类**

* `int read()`：读取单个字符
* `int read(char cbuf[], int off, int len)`：从第 off 位置开始读，读取 len 长度的字符，然后放入数组 b 中
* `long skip(long n)`：跳过指定个数的字符
* `int ready()`：是否可以读了
* `void close()`：关闭流，释放资源

**Writer 类**

* `void write(int c)`： 写入一个字符
* `void write( char cbuf[], int off, int len)`： 将数组 cbuf 中的从 off 位置开始，长度为 len 的字符写入
* `void flush()`： 强制刷新，将缓冲区的数据写入
* `void close()`：关闭流

## 操作对象分类

![](https://1780240882-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiUkJxV19BIulXoLHTeDI%2Fuploads%2Fgit-blob-d7ffe1964dc7846fffef7d14050189f5e72c0278%2Fshangtou-03.png?alt=media)

* **文件流**：直接操作文件的流，可以细分为字节流（FileInputStream 和 FileOuputStream）和字符流（FileReader 和 FileWriter）。还可以用于创建、删除、重命名文件等操作。
* **数组流**：针对文件的读写操作，使用文件流配合缓冲流就够用了，但为了提升效率，频繁地读写文件并不是太好，那么就出现了数组流，有时候也称为内存流。例如`ByteArrayInputStream` 。数组流可以用于在内存中读写数据，比如将数据存储在字节数组中进行压缩、加密、序列化等操作。它的优点是不需要创建临时文件，可以提高程序的效率。但是，数组流也有缺点，它只能存储有限的数据量，如果存储的数据量过大，会导致内存溢出。
* **管道**：Java 中管道要求通信的双方必须在**同一个进程**中，也就是在同一个 JVM 中，管道为线程之间的通信提供了通信能力。一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。可以实现不同线程之间的数据传输，可以用于线程间的通信、数据的传递等。
* **基本数据类型流**：字节流，该流不仅可以读写字节和字符，还可以读写基本数据类型。提供了一系列可以读写基本数据类型的方法
*   **缓冲**：为了减少程序和硬盘的交互，提升程序的效率，就引入了缓冲流，也就是类名前缀带有 Buffer 的那些，比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。即在内存中设置了一个缓冲区，只有缓冲区存储了足够多的带操作的数据后，才会和内存或者硬盘进行交互。简单来说，就是一次多读/写点，少读/写几次，这样程序的性能就会提高。但是，在使用缓冲流时需要注意缓冲区的大小和清空缓冲区的时机，以避免数据丢失或不完整的问题。

    ![](https://1780240882-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiUkJxV19BIulXoLHTeDI%2Fuploads%2Fgit-blob-35591754424e56aceea5c6ca706c58fa50f1156d%2Fshangtou-04.png?alt=media)
* **打印流**：用于打印输出数据的类，包括 PrintStream 和 PrintWriter 两个类。`System.out` 其实返回的就是一个 PrintStream 对象，可以用来打印各式各样的对象。PrintStream 最终输出的是字节数据，而 PrintWriter 则是扩展了 Writer 接口，所以它的 `print()/println()` 方法最终输出的是字符数据。
*   **对象序列化/反序列化**：序列化本质上是将一个 Java 对象转成**字节数组**，然后可以将其保存到文件中，或者通过网络传输到远程。反序列化，也就是再将字节数组转成 Java 对象的过程。

    ![](https://1780240882-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiUkJxV19BIulXoLHTeDI%2Fuploads%2Fgit-blob-49b9143f44a487e038a6ebccb392edaa34f8219b%2Fserialize-20230323105551.png?alt=media)
*   **转换**：InputStreamReader 是从**字节流到字符流**的桥连接，它使用指定的字符集读取字节并将它们解码为字符。OutputStreamWriter 将一个**字符流的输出对象变为字节流**的输出对象，是字符流通向字节流的桥梁。使用转换流可以方便地在字节流和字符流之间进行转换。在进行文本文件读写时，通常使用字符流进行操作，而在进行网络传输或与设备进行通信时，通常使用字节流进行操作。

    ![](https://1780240882-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiUkJxV19BIulXoLHTeDI%2Fuploads%2Fgit-blob-0bd66bd733f7402dab7dec2af73e39d23a91a3a6%2Fchar-byte-20230322165959.png?alt=media)

## NIO

![](https://1780240882-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FiUkJxV19BIulXoLHTeDI%2Fuploads%2Fgit-blob-4ac583cac053a954b838fa4cd5d149024c501413%2Fnio-better-io-20230406180538.png?alt=media)

传统 IO 基于字节流或字符流（如 FileInputStream、BufferedReader 等）进行文件读写，以及使用 Socket 和 ServerSocket 进行网络传输。

NIO 使用通道（Channel）和缓冲区（Buffer）进行文件操作，以及使用 SocketChannel 和 ServerSocketChannel 进行网络传输。

传统 IO 采用阻塞式模型，对于每个连接，都需要创建一个独立的线程来处理读写操作。当一个线程在等待 I/O 操作时，无法执行其他任务。这会导致大量线程的创建和销毁，以及上下文切换，降低了系统性能。

NIO 使用非阻塞模型，允许线程在等待 I/O 时执行其他任务。这种模式通过使用选择器（Selector）来监控多个通道（Channel）上的 I/O 事件，实现了更高的性能和可伸缩性。

> * nio 翻译成 no-blocking io 或者 new io 都无所谓啦，都说得通\~

JDK 1.4 中，`java.nio.*包`引入新的 Java I/O 库，其目的是**提高速度**。实际上，“旧”的 I/O 包已经使用 NIO**重新实现过，即使我们不显式的使用 NIO 编程，也能从中受益**。

NIO 的**魅力主要体现在网络中**！

NIO（New I/O）的设计目标是解决传统 I/O（BIO，Blocking I/O）在处理大量并发连接时的性能瓶颈。传统 I/O 在网络通信中主要使用阻塞式 I/O，为每个连接分配一个线程。当连接数量增加时，系统性能将受到严重影响，线程资源成为关键瓶颈。而 NIO 提供了非阻塞 I/O 和 I/O 多路复用，可以在单个线程中处理多个并发连接，从而在网络传输中显著提高性能。

以下是 NIO 在网络传输中优于传统 I/O 的原因：

1. NIO 支持非阻塞 I/O，这意味着在执行 I/O 操作时，线程不会被阻塞。这使得在网络传输中可以有效地管理大量并发连接（数千甚至数百万）。而在操作文件时，这个优势没有那么明显，因为文件读写通常不涉及大量并发操作。
2. NIO 支持 I/O 多路复用，这意味着一个线程可以同时监视多个通道（如套接字），并在 I/O 事件（如可读、可写）准备好时处理它们。这大大提高了网络传输中的性能，因为单个线程可以高效地管理多个并发连接。操作文件时这个优势也无法提现出来。
3. NIO 提供了 ByteBuffer 类，可以高效地管理缓冲区。这在网络传输中很重要，因为数据通常是以字节流的形式传输。操作文件的时候，虽然也有缓冲区，但优势仍然不够明显

## 三种IO区别

随着 Java 的发展，目前有三种 IO 共存。分别是 BIO、NIO 和 AIO。

* BIO 全称 Block-IO 是一种**同步且阻塞**的通信模式。是一个比较传统的通信方式，模式简单，使用方便。但并发处理能力低，通信耗时，依赖网速。
* Java NIO，全称 Non-Block IO ，是 Java SE 1.4 版以后，针对网络传输效能优化的新功能。是一种**非阻塞同步**的通信模式。 NIO 与原来的 I/O 有同样的作用和目的, 他们之间最重要的区别是数据打包和传输的方式。原来的 I/O 以流的方式处理数据，而 NIO 以块的方式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
* Java AIO，全称 Asynchronous IO，是**异步非阻塞**的 IO。是一种非阻塞异步的通信模式。

举个例子：

* 同步阻塞模式：这种模式下，我们的工作模式是先来到厨房，开始烧水，并坐在水壶面前**一直等**着水烧开。
* 同步非阻塞模式：这种模式下，我们的工作模式是先来到厨房，开始烧水，但是我们不一直坐在水壶前面等，而是回到客厅看电视，然后**每隔几分钟到厨房看一下**水有没有烧开。
* 异步非阻塞模式：这种模式下，我们的工作模式是先来到厨房，开始烧水，我们不一直坐在水壶前面等，也不隔一段时间去看一下，而是在客厅看电视，水壶上面有个开关，**水烧开之后他会通知我**。

适用场景：

* BIO 方式适用于**连接数目比较小且固定**的架构，这种方式对服务器资源要求比较高，并发局限于应用中，JDK1.4 以前的唯一选择，但程序直观简单易理解。
* NIO 方式适用于**高并发但是数据少**的架构，比如聊天服务器，并发局限于应用中，编程比较复杂，JDK1.4 开始支持。
* AIO 方式适用于**高并发高吞吐量**的的架构，比如相册服务器，充分调用 OS 参与并发操作，编程比较复杂，JDK7 开始支持。

## 多路复用

I/O 多路复用（I/O Multiplexing）模型使用操作系统提供的多路复用功能（如 select、poll、epoll 等），使得单个线程可以同时处理多个 I/O 事件。当某个连接上的数据准备好时，操作系统会通知应用程序。这样，应用程序可以在一个线程中处理多个并发连接，而不需要为每个连接创建一个线程。

* select 是 Unix 系统中最早的 I/O 多路复用技术。它**允许一个线程同时监视多个文件描述符**（如套接字），并等待某个文件描述符上的 I/O 事件（如可读、可写或异常）。select 的主要问题是性能受限，特别是在处理大量文件描述符时。这是因为它使用一个**位掩码**来表示文件描述符集，每次调用都需要传递这个掩码，并在内核和用户空间之间进行复制。
* poll 是对 select 的改进。它使用一个**文件描述符数组**而不是位掩码来表示文件描述符集。这样可以避免 select 中的性能问题。然而，poll 仍然需要遍历整个文件描述符数组，以检查每个文件描述符的状态。因此，在处理大量文件描述符时，性能仍然受限。
* epoll 是 Linux 中的一种高性能 I/O 多路复用技术。它通过在**内核中维护一个事件表**来避免遍历文件描述符数组的性能问题。当某个文件描述符上的 I/O 事件发生时，内核会将该事件添加到事件表中。应用程序可以使用 epoll\_wait 函数来获取已准备好的 I/O 事件，而无需遍历整个文件描述符集。这种方法大大提高了在大量并发连接下的性能。

在 Java NIO 中，I/O 多路复用主要通过 Selector 类实现。Selector 能够监控多个 Channel（通道）上的 I/O 事件，如连接、读取和写入。这使得一个线程可以处理多个并发连接，提高了程序的性能和可伸缩性。

## 信号驱动

信号驱动 I/O（Signal-driven I/O）模型中，应用程序可以**向操作系统注册一个信号处理函数**，当某个 I/O 事件发生时，操作系统会发送一个信号通知应用程序。应用程序在收到信号后处理相应的 I/O 事件。这种模型与非阻塞 I/O 类似，也需要在应用程序级别进行事件管理和调度。

多路复用和信号驱动的差别主要在事件通知机制和引用场景上。

* 多路复用模型允许一个线程同时管理多个 I/O 连接。这是通过使用特殊的**系统调用**（如 select、poll 和 epoll）实现的，它们能够监视多个文件描述符上的 I/O 事件。当某个 I/O 事件发生时，这些系统调用会返回，通知应用程序执行相应的 I/O 操作。I/O 多路复用模型**适用于高并发、低延迟和高吞吐量的场景**，因为它能够有效地减少线程数量和上下文切换开销。
* 信号驱动模型依赖于**信号**（如 SIGIO）来通知应用程序 I/O 事件的发生。在这个模型中，应用程序首先设置文件描述符为信号驱动模式，并为相应的信号注册处理函数。当 I/O 事件发生时，内核会发送一个信号给应用程序，触发信号处理函数的执行。然后，应用程序可以在信号处理函数中执行相应的 I/O 操作。I/O 信号驱动模型适用于**低并发、低延迟和低吞吐量**的场景，因为它需要为每个 I/O 事件创建一个信号和信号处理函数。

## 异步 IO

异步 I/O（Asynchronous I/O）模型与同步 I/O 模型的主要区别在于，异步 I/O 操作会在后台运行，当操作完成时，操作系统会通知应用程序。应用程序不需要等待 I/O 操作的完成，可以继续执行其他任务。这种模型适用于处理大量并发连接，且可以简化应用程序的设计和开发。

* 同步：在执行 I/O 操作时，应用程序需要等待操作的完成。同步操作会导致线程阻塞，直到操作完成。同步 I/O 包括阻塞 I/O、非阻塞 I/O 和 I/O 多路复用。
* 异步：在执行 I/O 操作时，应用程序不需要等待操作的完成。异步操作允许应用程序在 I/O 操作进行时继续执行其他任务。异步 I/O 模型包括信号驱动 I/O 和异步 I/O。
