一、IO
IO 是 Input 和 Output 二词的缩写,意为输入和输出,直接来说,实现一般的 I/O 是没有什么难度的,但涉及到多线程时,要解决 I/O 的问题就不是一个简单的事情了,会涉及到同步和异步的问题,阻塞和非阻塞的问题。
1.1 同步和异步
同步可以借用多线程来方便理解,多条线程,从字面意思上来看,当他们在同一直线上时,就是同步,反之则是异步。那什么是在同一直线上呢?简单说就是,它们都在处理同时事件,比如同时运行某个函数,调用或修改某个变量。
从这里我们不难看出,同步有时是会产生一些问题的,比如同时修改某个变量。从现实角度来看,这是不可能修改成功的,毕竟是完完全全同时嘛。但是,我们知道计算机的每个 CPU 执行多线程实际并非真的同时干两个事情,它只是将时间分片了,一会儿做这个线程的事情,一会做另外一个线程的事情,由于 CPU 切换任务和执行任务的速度非常非常快,因此从宏观时间尺度上来看,就好像多个线程在同时运行,但实际在微观时间尺度上,它们仍然是单线程的。
多线程
接上面的说,那么计算机的两个线程同时修改某个变量后,必然有一个线程先对这个变量进行修改,另外一个线程再对这个线程进行修改,由于分片数量和执行速度不能保证完完全全相同,所以我们无法预测到底是哪个线程先对这个变量进行的修改,那么这就会产生一系列无法预知的问题。为此,异步操作诞生了!
从程序优化的角度上看,异步操作优于同步操作,但相应地,实现难度会大一些。
1.2 阻塞和非阻塞
最典型的阻塞就是终端等待用户输入,Java 里面使用 Scanner 类时,在不做多线程等处理时,要一直等到用户在终端输入字符后,Scanner 类后面的代码才能运行。而非阻塞呢,在界面编程里面体现的非常明显,窗口的显示实际可以看作是一个大循环,它在每一次循环中都在刷新着窗口,但我们在窗口中的操作并运行一些计算的时候呢,界面也一直都在,它并不会因为我们操作了什么而产生了阻塞,导致窗口没有变化(不刷新)了。
从阻塞和非阻塞的含义上来看,大部分时间我们都希望程序是非阻塞的,因为阻塞的情况下,阻塞处后面的程序无法运行,这就浪费计算机的性能了,等阻塞完之后再执行后面的程序,会产生时间上的消耗,使用户产生延迟感,这是不好的,为了解决这个问题,非阻塞操作产生了!
从程序优化的角度上看,非阻塞操作优于阻塞操作,但相应地,非阻塞的实现难度大一些。
了解了(非)同步和(非)阻塞之后,我们再来看 I/O,根据是否同步和是否阻塞以及按它们出现的时间顺序,主要划分为 3 种 I/O 技术,分别是 BIO、NIO 和 AIO。当然,并不是只有这几种,还有其他的 I/O 类型。
二、BIO
BIO 是 Blocking I/O 的缩写,意为同步阻塞式 I/O,Blocking 是阻塞的意思。
BIO 是最基本的 I/O 处理模式。在这种模式下,当一个 I/O 操作正在进行时,会阻塞其他所有操作,直到这个 I/O 操作完成。这种模式的优点是编程模型简单,适合请求不高并发、任务简单的场景。但是,对于高并发的场景,BIO 可能会导致大量的线程阻塞,消耗大量的系统资源,性能较低。
三、NIO
NIO 是 Non-blocking I/O 的缩写,代表同步非阻塞式 I/O,Non-blocking 是非阻塞意思。
NIO 是对 BIO 的一种改进。在这种模式下,当一个 I/O 操作正在进行时,不会阻塞其他操作。NIO 通过使用 Buffer(缓冲区)和 Channel(通道)进行数据的传输,以及使用 Selector(选择器)进行多路复用,可以同时处理多个客户端的连接和请求。这种模式的优点是可以处理高并发的场景,但是编程模型相对复杂。
下面是 NIO 的主要组件:
Channel(通道):Channel 是一个可以进行 I/O 操作的连接点。所有的数据都必须通过 Channel 读取或者写入。Channel 的主要实现包括 FileChannel(用于文件 I/O)、DatagramChannel(用于 UDP I/O)和 SocketChannel(用于 TCP I/O)。
Buffer(缓冲区):Buffer 是一个容器,用于在 Channel 和程序之间传输数据。当我们从 Channel 读取数据时,数据会被读入 Buffer;当我们向 Channel 写入数据时,数据会从 Buffer 写入。
Selector(选择器):Selector 是一个可以监控多个 Channel 的 I/O 状态(如:连接、读取、写入)的组件。通过 Selector,我们可以使用一个线程来处理多个 Channel 的 I/O 操作。
虽然 NIO 可以处理高并发场景,但当并发数量过大时,由于 NIO 的原理,它仍然会出现类似 BIO 阻塞的情况。
四、AIO
AIO 是 Asynchronous I/O 的缩写,意为异步非阻塞式 I/O,Asynchronous 是异步的意思。按照我们之前说的,这已经属于是效果最好,实现最难的 I/O 了。
AIO 是一种更高级的 I/O 处理模式。在这种模式下,一个 I/O 操作可以在后台异步地进行,当操作完成时,会通知相应的线程进行处理。这种模式的优点是可以处理高并发的场景,并且编程模型相对简单。但是,由于 AIO 是在操作系统级别进行异步操作,所以对操作系统的要求较高。
五、总结
按照时间出现的顺序,分别是 BIO、NIO 和 AIO,按照技术实现难度,分别是 BIO、NIO 和 AIO。总结为下表:
|---------|----------------|-----------------|-----------------|
| I/O | BIO(同步阻塞式) | NIO(同步非阻塞式) | AIO(异步非阻塞式) |
| 是否同步 | 否 | 否 | 是 |
| 是否非阻塞 | 否 | 是 | 是 |
| 出现时间 | 最早 | 中等 | 最迟 |
| 实现难度 | 简单 | 中等 | 困难 |
| 实现效果 | 差 | 中等 | 好 |
| 代码维护难度 | 小 | 中等 | 大 |
这三者的应用场景如下:
- BIO 适用于单线程或少量并发的场景,每个 I/O 操作都会阻塞当前线程;
- NIO 适用于需要处理大量并发连接的场景,一个线程可以同时处理多个 I/O 操作;
- AIO 适用于需要处理大量并发操作且不希望线程阻塞的场景,通过回调函数处理 I/O 操作的完成;
有人就要问了,怎么没有 异步阻塞式 I/O 呢?这是个好问题。
异步阻塞式 I/O 也不是没有,只不过它的叫法与其他的不太一样,它叫做I/O 多路复用(I/O Multiplexing)。这里不详细展开讲述了,后面会单独出一片文章进行讲解。