Java_IO流面试

Java IO基础知识总结
IO 流简介
IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
字节流
InputStream(字节输入流)
InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类。
InputStream 常用方法 :
read():返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回-1,表示文件结束。read(byte b[ ]): 从输入流中读取一些字节存储到数组b中。如果数组b的长度为零,则不读取。如果没有可用字节读取,返回-1。如果有可用字节读取,则最多读取的字节数最多等于b.length, 返回读取的字节数。这个方法等价于read(b, 0, b.length)。read(byte b[], int off, int len):在read(byte b[ ])方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。skip(long n):忽略输入流中的 n 个字节 ,返回实际忽略的字节数。available():返回输入流中可以读取的字节数。close():关闭输入流释放相关的系统资源。
从 Java 9 开始,InputStream 新增加了多个实用的方法:
readAllBytes():读取输入流中的所有字节,返回字节数组。readNBytes(byte[] b, int off, int len):阻塞直到读取len个字节。transferTo(OutputStream out): 将所有字节从一个输入流传递到一个输出流。
FileInputStream 是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。
FileInputStream 代码示例:
1 | try (InputStream fis = new FileInputStream("input.txt")) { |
input.txt 文件内容:

输出:
1 | Number of remaining bytes:11 |
不过,一般我们是不会直接单独使用 FileInputStream ,通常会配合 BufferedInputStream(字节缓冲输入流,后文会讲到)来使用。
像下面这段代码在我们的项目中就比较常见,我们通过 readAllBytes() 读取输入流所有字节并将其直接赋值给一个 String 对象。
1 | // 新建一个 BufferedInputStream 对象 |
DataInputStream 用于读取指定类型数据,不能单独使用,必须结合 FileInputStream 。
1 | FileInputStream fileInputStream = new FileInputStream("input.txt"); |
ObjectInputStream 用于从输入流中读取 Java 对象(反序列化),ObjectOutputStream 用于将对象写入到输出流(序列化)。
1 | ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data")); |
另外,用于序列化和反序列化的类必须实现 Serializable 接口,对象中如果有属性不想被序列化,使用 transient 修饰。
OutputStream(字节输出流)
OutputStream用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream抽象类是所有字节输出流的父类。
OutputStream 常用方法 :
write(int b):将特定字节写入输出流。write(byte b[ ]): 将数组b写入到输出流,等价于write(b, 0, b.length)。write(byte[] b, int off, int len): 在write(byte b[ ])方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。flush():刷新此输出流并强制写出所有缓冲的输出字节。close():关闭输出流释放相关的系统资源。
FileOutputStream 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。
FileOutputStream 代码示例:
1 | try (FileOutputStream output = new FileOutputStream("output.txt")) { |
运行结果:

类似于 FileInputStream,FileOutputStream 通常也会配合 BufferedOutputStream(字节缓冲输出流,后文会讲到)来使用。
1 | FileOutputStream fileOutputStream = new FileOutputStream("output.txt"); |
DataOutputStream 用于写入指定类型数据,不能单独使用,必须结合 FileOutputStream
1 | // 输出流 |
ObjectOutputStream 用于从输入流中读取 Java 对象(ObjectInputStream,反序列化)或者将对象写入到输出流(ObjectOutputStream,序列化)。
1 | ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt") |
字符流
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。 那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
个人认为主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 如果我们不知道编码类型就很容易出现乱码问题。
乱码问题这个很容易就可以复现,我们只需要将上面提到的 FileInputStream 代码示例中的 input.txt 文件内容改为中文即可,原代码不需要改动。

输出:
1 | Number of remaining bytes:9 |
可以很明显地看到读取出来的内容已经变成了乱码。
因此,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
字符流默认采用的是 Unicode 编码,我们可以通过构造方法自定义编码。顺便分享一下之前遇到的笔试题:常用字符编码所占字节数?utf8 :英文占 1 字节,中文占 3 字节,unicode:任何字符都占 2 个字节,gbk:英文占 1 字节,中文占 2 字节。
Reader(字符输入流)
Reader用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader抽象类是所有字符输入流的父类。
Reader 用于读取文本, InputStream 用于读取原始字节。
Reader 常用方法 :
read(): 从输入流读取一个字符。read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组cbuf中,等价于read(cbuf, 0, cbuf.length)。read(char[] cbuf, int off, int len):在read(char[] cbuf)方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。skip(long n):忽略输入流中的 n 个字符 ,返回实际忽略的字符数。close(): 关闭输入流并释放相关的系统资源。
InputStreamReader 是字节流转换为字符流的桥梁,其子类 FileReader 是基于该基础上的封装,可以直接操作字符文件。
1 | // 字节流转换为字符流的桥梁 |
FileReader 代码示例:
1 | try (FileReader fileReader = new FileReader("input.txt");) { |
input.txt 文件内容:

输出:
1 | The actual number of bytes skipped:3 |
Writer(字符输出流)
Writer用于将数据(字符信息)写入到目的地(通常是文件),java.io.Writer抽象类是所有字节输出流的父类。
Writer 常用方法 :
write(int c): 写入单个字符。write(char[] cbuf):写入字符数组cbuf,等价于write(cbuf, 0, cbuf.length)。write(char[] cbuf, int off, int len):在write(char[] cbuf)方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。write(String str):写入字符串,等价于write(str, 0, str.length())。write(String str, int off, int len):在write(String str)方法的基础上增加了off参数(偏移量)和len参数(要读取的最大字节数)。append(CharSequence csq):将指定的字符序列附加到指定的Writer对象并返回该Writer对象。append(char c):将指定的字符附加到指定的Writer对象并返回该Writer对象。flush():刷新此输出流并强制写出所有缓冲的输出字符。close():关闭输出流释放相关的系统资源。
OutputStreamWriter 是字符流转换为字节流的桥梁,其子类 FileWriter 是基于该基础上的封装,可以直接将字符写入到文件。
1 | // 字符流转换为字节流的桥梁 |
FileWriter 代码示例:
1 | try (Writer output = new FileWriter("output.txt")) { |
输出结果:

字节缓冲流
IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。
字节缓冲流这里采用了装饰器模式来增强 InputStream 和OutputStream子类对象的功能。
举个例子,我们可以通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream 的功能。
1 | // 新建一个 BufferedInputStream 对象 |
字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用 write(int b) 和 read() 这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。
我使用 write(int b) 和 read() 方法,分别通过字节流和字节缓冲流复制一个 524.9 mb 的 PDF 文件耗时对比如下:
1 | 使用缓冲流复制PDF文件总耗时:15428 毫秒 |
两者耗时差别非常大,缓冲流耗费的时间是字节流的 1/165。
测试代码如下:
1 |
|
如果是调用 read(byte b[]) 和 write(byte b[], int off, int len) 这两个写入一个字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。
这次我们使用 read(byte b[]) 和 write(byte b[], int off, int len) 方法,分别通过字节流和字节缓冲流复制一个 524.9 mb 的 PDF 文件耗时对比如下:
1 | 使用缓冲流复制PDF文件总耗时:695 毫秒 |
两者耗时差别不是很大,缓冲流的性能要略微好一点点。
测试代码如下:
1 |
|
BufferedInputStream(字节缓冲输入流)
BufferedInputStream 从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。
BufferedInputStream 内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组,通过阅读 BufferedInputStream 源码即可得到这个结论。
1 | public |
缓冲区的大小默认为 8192 字节,当然了,你也可以通过 BufferedInputStream(InputStream in, int size) 这个构造方法来指定缓冲区的大小。
BufferedOutputStream(字节缓冲输出流)
BufferedOutputStream 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率
1 | try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) { |
类似于 BufferedInputStream ,BufferedOutputStream 内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节。
字符缓冲流
BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
打印流
下面这段代码大家经常使用吧?
1 | System.out.print("Hello!"); |
System.out 实际是用于获取一个 PrintStream 对象,print方法实际调用的是 PrintStream 对象的 write 方法。
PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)。PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类。
1 | public class PrintStream extends FilterOutputStream |
随机访问流
这里要介绍的随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile 。
RandomAccessFile 的构造方法如下,我们可以指定 mode(读写模式)。
1 | // openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除 |
读写模式主要有下面四种:
r: 只读模式。rw: 读写模式rws: 相对于rw,rws同步更新对“文件的内容”或“元数据”的修改到外部存储设备。rwd: 相对于rw,rwd同步更新对“文件的内容”的修改到外部存储设备。
文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
RandomAccessFile 中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。我们可以通过 RandomAccessFile 的 seek(long pos) 方法来设置文件指针的偏移量(距文件开头 pos 个字节处)。如果想要获取文件指针当前的位置的话,可以使用 getFilePointer() 方法。
RandomAccessFile 代码示例:
1 | RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); |
input.txt 文件内容:

输出:
1 | 读取之前的偏移量:0,当前读取到的字符A,读取之后的偏移量:1 |
input.txt 文件内容变为 ABCDEFGHIJK 。
RandomAccessFile 的 write 方法在写入对象的时候如果对应的位置已经有数据的话,会将其覆盖掉。
1 | RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw"); |
假设运行上面这段程序之前 input.txt 文件内容变为 ABCD ,运行之后则变为 HIJK 。
RandomAccessFile 比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。
RandomAccessFile 可以帮助我们合并文件分片,示例代码如下:

我在《Java 面试指北》open in new window中详细介绍了大文件的上传问题。

RandomAccessFile 的实现依赖于 FileDescriptor (文件描述符) 和 FileChannel (内存映射文件)。
Java IO设计模式总结
这篇文章我们简单来看看我们从 IO 中能够学习到哪些设计模式的应用。
装饰器模式
装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能。
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
对于字节流来说, FilterInputStream (对应输入流)和FilterOutputStream(对应输出流)是装饰器模式的核心,分别用于增强 InputStream 和OutputStream子类对象的功能。
我们常见的BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。
举个例子,我们可以通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream 的功能。
BufferedInputStream 构造函数如下:
1 | public BufferedInputStream(InputStream in) { |
可以看出,BufferedInputStream 的构造函数其中的一个参数就是 InputStream 。
BufferedInputStream 代码示例:
1 | try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) { |
这个时候,你可以会想了:为啥我们直接不弄一个BufferedFileInputStream(字符缓冲文件输入流)呢?
1 | BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt"); |
如果 InputStream的子类比较少的话,这样做是没问题的。不过, InputStream的子类实在太多,继承关系也太复杂了。如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。
如果你对 IO 流比较熟悉的话,你会发现ZipInputStream 和ZipOutputStream 还可以分别增强 BufferedInputStream 和 BufferedOutputStream 的能力。
1 | BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName)); |
ZipInputStream 和ZipOutputStream 分别继承自InflaterInputStream 和DeflaterOutputStream。
1 | public |
这也是装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。
为了实现这一效果,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。上面介绍到的这些 IO 相关的装饰类和原始类共同的父类是 InputStream 和OutputStream。
对于字符流来说,BufferedReader 可以用来增加 Reader (字符输入流)子类的功能,BufferedWriter 可以用来增加 Writer (字符输出流)子类的功能。
1 | BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8")); |
IO 流中的装饰器模式应用的例子实在是太多了,不需要特意记忆,完全没必要哈!搞清了装饰器模式的核心之后,你在使用的时候自然就会知道哪些地方运用到了装饰器模式。
适配器模式
适配器(Adapter Pattern)模式 主要用于接口互不兼容的类的协调工作,你可以将其联想到我们日常经常使用的电源适配器。
适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。
InputStreamReader 和 OutputStreamWriter 就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。InputStreamReader 使用 StreamDecoder (流解码器)对字节进行解码,实现字节流到字符流的转换, OutputStreamWriter 使用StreamEncoder(流编码器)对字符进行编码,实现字符流到字节流的转换。
InputStream 和 OutputStream 的子类是被适配者, InputStreamReader 和 OutputStreamWriter是适配器。
1 | // InputStreamReader 是适配器,FileInputStream 是被适配的类 |
java.io.InputStreamReader 部分源码:
1 | public class InputStreamReader extends Reader { |
java.io.OutputStreamWriter 部分源码:
1 | public class OutputStreamWriter extends Writer { |
适配器模式和装饰器模式有什么区别呢?
装饰器模式 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。并且,装饰器模式支持对原始类嵌套使用多个装饰器。
适配器模式 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。就比如说 StreamDecoder (流解码器)和StreamEncoder(流编码器)就是分别基于 InputStream 和 OutputStream 来获取 FileChannel对象并调用对应的 read 方法和 write 方法进行字节数据的读取和写入。
1 | StreamDecoder(InputStream in, Object lock, CharsetDecoder dec) { |
适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。
另外,FutrueTask 类使用了适配器模式,Executors 的内部类 RunnableAdapter 实现属于适配器,用于将 Runnable 适配成 Callable。
FutureTask参数包含 Runnable 的一个构造方法:
1 | public FutureTask(Runnable runnable, V result) { |
Executors中对应的方法和适配器:
1 | // 实际调用的是 Executors 的内部类 RunnableAdapter 的构造方法 |
工厂模式
工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 Files 类的 newInputStream 方法用于创建 InputStream 对象(静态工厂)、 Paths 类的 get 方法创建 Path 对象(静态工厂)、ZipFileSystem 类(sun.nio包下的类,属于 java.nio 相关的一些内部实现)的 getPath 的方法创建 Path 对象(简单工厂)。
1 | InputStream is Files.newInputStream(Paths.get(generatorLogoPath)) |
观察者模式
NIO 中的文件目录监听服务使用到了观察者模式。
NIO 中的文件目录监听服务基于 WatchService 接口和 Watchable 接口。WatchService 属于观察者,Watchable 属于被观察者。
Watchable 接口定义了一个用于将对象注册到 WatchService(监控服务) 并绑定监听事件的方法 register 。
1 | public interface Path |
WatchService 用于监听文件目录的变化,同一个 WatchService 对象能够监听多个文件目录。
1 | // 创建 WatchService 对象 |
Path 类 register 方法的第二个参数 events (需要监听的事件)为可变长参数,也就是说我们可以同时监听多种事件。
1 | WatchKey register(WatchService watcher, |
常用的监听事件有 3 种:
StandardWatchEventKinds.ENTRY_CREATE:文件创建。StandardWatchEventKinds.ENTRY_DELETE: 文件删除。StandardWatchEventKinds.ENTRY_MODIFY: 文件修改。
register 方法返回 WatchKey 对象,通过WatchKey 对象可以获取事件的具体信息比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。
1 | WatchKey key; |
WatchService 内部是通过一个 daemon thread(守护线程)采用定期轮询的方式来检测文件的变化,简化后的源码如下所示。
1 | class PollingWatchService |
参考
- Patterns in Java APIs:http://cecs.wright.edu/~tkprasad/courses/ceg860/paper/node26.html
- 装饰器模式:通过剖析 Java IO 类库源码学习装饰器模式:https://time.geekbang.org/column/article/204845
- sun.nio 包是什么,是 java 代码么? - RednaxelaFX https://www.zhihu.com/question/29237781/answer/43653953
Java IO模型详解
IO 模型这块确实挺难理解的,需要太多计算机底层知识。写这篇文章用了挺久,就非常希望能把我所知道的讲出来吧!希望朋友们能有收获!为了写这篇文章,还翻看了一下《UNIX 网络编程》这本书,太难了,我滴乖乖!心痛~
个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!
前言
I/O 一直是很多小伙伴难以理解的一个知识点,这篇文章我会将我所理解的 I/O 讲给你听,希望可以对你有所帮助。
何为 I/O?
I/O(Input/Outpu) 即输入/输出 。
我们先从计算机结构的角度来解读一下 I/O。
根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。

输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
我们再先从应用程序的角度来解读一下 I/O。
根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和响应)。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
有哪些常见的 IO 模型?
UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
这也是我们经常提到的 5 种 IO 模型。
Java 中 3 种常见 IO 模型
BIO (Blocking I/O)
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO (Non-blocking/New I/O)
Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。
Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
跟着我的思路往下看看,相信你会得到答案!
我们先来看看 同步非阻塞 IO 模型。

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
这个时候,I/O 多路复用模型 就上场了。

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。
- select 调用 :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
- epoll 调用 :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

AIO (Asynchronous I/O)
AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。
最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。

参考
- 《深入拆解 Tomcat & Jetty》
- 如何完成一次 IO:https://llc687.top/126.html
- 程序员应该这样理解 IO:https://www.jianshu.com/p/fa7bdc4f3de7open in new window
- 10 分钟看懂, Java NIO 底层原理:https://www.cnblogs.com/crazymakercircle/p/10225159.html
- IO 模型知多少 | 理论篇:https://www.cnblogs.com/sheng-jie/p/how-much-you-know-about-io-models.html
- 《UNIX 网络编程 卷 1;套接字联网 API 》6.2 节 IO 模型