Java IO
2022-07-27 11:50:37 126 举报
AI智能生成
Java IO(输入/输出)是Java编程语言中的一个核心概念,用于处理数据的读取和写入操作。它提供了一套丰富的类和接口,使得程序能够与外部资源(如文件、网络连接等)进行交互。Java IO主要包括字节流和字符流两种类型,分别用于处理以字节为单位和以字符为单位的数据。通过使用Java IO,程序员可以方便地读取文件内容、写入数据到文件、实现网络通信等功能。Java IO还支持缓冲流、过滤流等高级特性,以提高数据处理的效率。总之,Java IO是Java编程中不可或缺的一部分,它为开发人员提供了强大的工具来处理各种输入输出任务。
作者其他创作
大纲/内容
Java IO 基础知识
概述
IO 即 Input/Output,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部
存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。
存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。
IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的:
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的:
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
关系类图
InputStream
OutputStream
Reader
Writer
字节流
InputStream(字节输入流)
InputStream 用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream 抽象类是所有字节输入流的父类。
InputStream 常用方法
read() :返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回 -1 ,表示文件结束。
read(byte b[ ]) : 从输入流中读取一些字节存储到数组 b 中。如果数组 b 的长度为零,则不读取。如果没有可用字节读取,返回 -1。
如果有可用字节读取,则最多读取的字节数最多等于 b.length , 返回读取的字节数。这个方法等价于 read(b, 0, b.length)。
如果有可用字节读取,则最多读取的字节数最多等于 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 是一个比较常用的字节输入流对象,可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。
示例
代码:
input.txt 文件内容:
使用技巧
一般我们是不会直接单独使用 FileInputStream ,通常会配合 BufferedInputStream(字节缓冲输入流,下面会讲到)来使用。
像下面这段代码在我们的项目中就比较常见,我们通过 readAllBytes() 读取输入流所有字节并将其直接赋值给一个 String 对象:
DataInputStream
DataInputStream 用于读取指定类型数据,不能单独使用,必须结合 FileInputStream :
ObjectInputStream
ObjectInputStream 用于从输入流中读取 Java 对象(反序列化),ObjectOutputStream 用于将对象写入到输出流 (序列化)。
注:用于序列化和反序列化的类必须实现 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 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。
示例
代码:
输出文件 output.txt:
使用技巧
类似于 FileInputStream,FileOutputStream 通常也会配合 BufferedOutputStream(字节缓冲输出流,下面会讲到)来使用:
DataOutputStream
DataOutputStream 用于写入指定类型数据,不能单独使用,必须结合 FileOutputStream:
ObjectOutputStream
ObjectInputStream 用于从输入流中读取 Java 对象(反序列化),ObjectOutputStream 用于将对象写入到输出流 (序列化)
字符流
为什么要有字符流
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。 那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
- 如果我们不知道编码类型就很容易出现乱码问题。
因此,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 是基于该基础上的封装,可以直接操作字符文件。
FileReader 代码示例:
input.txt 文件内容:
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 是基于该基础上的封装,可以直接将字符写入到文件。
FileWriter 代码示例:
输出结果:
字节缓冲流
为什么要有缓冲流
IO 操作是很消耗性能的,缓冲流可以将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。
字节缓冲流这里采用了装饰器模式来增强 InputStream 和OutputStream子类对象的功能。
字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用 write(int b) 和 read() 这两个一次只读取一个字节的方法的时候。
由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。
由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。
如果是调用 read(byte b[]) 和 write(byte b[], int off, int len) 这两个写入一个字节数组的方法的话,
只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。
只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。
BufferedInputStream(字节缓冲输入流)
BufferedInputStream 从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,
而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。
而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。
BufferedInputStream 内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组,通过阅读 BufferedInputStream 源码即可得到这个结论。
缓冲区的大小默认为 8192 字节,当然了,也可以通过 BufferedInputStream(InputStream in, int size) 这个构造方法来指定缓冲区的大小。
BufferedOutputStream(字节缓冲输出流)
BufferedOutputStream 将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,
而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率。
而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率。
示例,往 output.txt 文件中写内容:
类似于 BufferedInputStream ,BufferedOutputStream 内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节。
字符缓冲流
BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和
BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
打印流
System.out 实际是用于获取一个 PrintStream 对象,print方法实际调用的是 PrintStream 对象的 write 方法。
PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)。
PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类。
PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类。
随机访问流
随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile 。
RandomAccessFile 的构造方法如上,我们可以指定 mode(读写模式):
- r : 只读模式。
- rw: 读写模式
- rws: 相对于 rw,rws 同步更新对“文件的内容”或“元数据”的修改到外部存储设备。
- rwd : 相对于 rw,rwd 同步更新对“文件的内容”的修改到外部存储设备。
RandomAccessFile 比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。
RandomAccessFile 可以帮助我们合并文件分片,示例代码如下:
分片上传常常使用在大文件的上传问题中,《Java 面试指北》中详细介绍了大文件的上传问题
Java IO 设计模式
装饰器模式
装饰器(Decorator)模式可以 在不改变原有对象的情况下拓展其功能。
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
装饰器模式通过组合替代继承来扩展原始类的功能,在一些继承关系比较复杂的场景(IO 这一场景各种类的继承关系就比较复杂)更加实用。
对于字节流来说, FilterInputStream (对应输入流)和FilterOutputStream(对应输出流)是装饰器模式的核心,
分别用于增强 InputStream 和OutputStream子类对象的功能。
我们常见的BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,
BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。
分别用于增强 InputStream 和OutputStream子类对象的功能。
我们常见的BufferedInputStream(字节缓冲输入流)、DataInputStream 等等都是FilterInputStream 的子类,
BufferedOutputStream(字节缓冲输出流)、DataOutputStream等等都是FilterOutputStream的子类。
示例
通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream 的功能。
BufferedInputStream 构造函数如下,可以看出,BufferedInputStream
的构造函数其中的一个参数就是 InputStream(FileInputStream 的父类)
的构造函数其中的一个参数就是 InputStream(FileInputStream 的父类)
BufferedInputStream 代码示例:
为何不直接弄一个 BufferedInputStream?
可以直接使用 BufferedInputStream 读取文件多好:
BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt");
BufferedFileInputStream bfis = new BufferedFileInputStream("input.txt");
如果 InputStream的子类比较少的话,这样做是没问题的。不过, InputStream的子类实在太多,继承关系也太复杂了。
如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。
如果我们为每一个子类都定制一个对应的缓冲输入流,那岂不是太麻烦了。
如果对 IO 流比较熟悉的话,会发现 ZipInputStream 和 ZipOutputStream 还可以分别增强 BufferedInputStream 和 BufferedOutputStream。
ZipInputStream 和ZipOutputStream 分别继承自InflaterInputStream 和DeflaterOutputStream。
这也是装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。
ZipInputStream 和ZipOutputStream 分别继承自InflaterInputStream 和DeflaterOutputStream。
这也是装饰器模式很重要的一个特征,那就是可以对原始类嵌套使用多个装饰器。
为了实现这一效果,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。
上面介绍到的这些 IO 相关的装饰类和原始类共同的父类是 InputStream 和OutputStream。
上面介绍到的这些 IO 相关的装饰类和原始类共同的父类是 InputStream 和OutputStream。
对于字符流来说,BufferedReader 可以用来增加 Reader (字符输入流)子类的功能,BufferedWriter 可以用来增加 Writer (字符输出流)子类的功能:
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8"));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8"));
适配器模式
适配器(Adapter Pattern)模式 主要用于 接口互不兼容的类的协调工作,可以将其联想到我们日常经常使用的电源适配器。
适配器模式中存在被适配的对象或者类称为 适配者(Adaptee) ,作用于适配者的对象或者类称为适配器(Adapter) 。
适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
适配器分为对象适配器和类适配器。类适配器使用继承关系来实现,对象适配器使用组合关系来实现。
IO 流中的字符流和字节流的接口不同,它们之间可以协调工作就是基于适配器模式来做的,更准确点来说是对象适配器。通过适配器,我们可以将字节流对象适配成一个字符流对象,这样我们可以直接通过字节流对象来读取或者写入字符数据。
示例
InputStreamReader 和 OutputStreamWriter 就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。
- InputStreamReader 使用 StreamDecoder (流解码器)对字节进行解码,实现字节流到字符流的转换;
- OutputStreamWriter 使用StreamEncoder(流编码器)对字符进行编码,实现字符流到字节流的转换。
InputStream 和 OutputStream 的子类是被适配者, InputStreamReader 和 OutputStreamWriter是适配器。
适配器模式和装饰器模式的区别
装饰器模式 更侧重于动态地增强原始类的功能,装饰器类需要跟原始类继承相同的抽象类或者实现相同的接口。
并且,装饰器模式支持对原始类嵌套使用多个装饰器。
并且,装饰器模式支持对原始类嵌套使用多个装饰器。
适配器模式 更侧重于让接口不兼容而不能交互的类可以一起工作,当我们调用适配器对应的方法时,适配器内部会调用适配者类或者和适配类相关的类的方法,这个过程透明的。就比如说 StreamDecoder (流解码器)和StreamEncoder(流编码器)就是分别基于 InputStream 和 OutputStream 来获取 FileChannel对象并调用对应的 read 方法和 write 方法进行字节数据的读取和写入。
适配器和适配者两者不需要继承相同的抽象类或者实现相同的接口。
工厂模式
工厂模式用于创建对象,NIO 中大量用到了工厂模式。
比如 Files 类的 newInputStream 方法用于创建 InputStream 对象(静态工厂)、 Paths 类的 get 方法创建 Path 对象(静态工厂)、
ZipFileSystem 类(sun.nio包下的类,属于 java.nio 相关的一些内部实现)的 getPath 的方法创建 Path 对象(简单工厂)。
InputStream is Files.newInputStream(Paths.get(generatorLogoPath))
比如 Files 类的 newInputStream 方法用于创建 InputStream 对象(静态工厂)、 Paths 类的 get 方法创建 Path 对象(静态工厂)、
ZipFileSystem 类(sun.nio包下的类,属于 java.nio 相关的一些内部实现)的 getPath 的方法创建 Path 对象(简单工厂)。
InputStream is Files.newInputStream(Paths.get(generatorLogoPath))
观察者模式
NIO 中的文件目录监听服务使用到了观察者模式。
NIO 中的文件目录监听服务基于 WatchService 接口和 Watchable 接口。WatchService 属于观察者,Watchable 属于被观察者。
NIO 中的文件目录监听服务基于 WatchService 接口和 Watchable 接口。WatchService 属于观察者,Watchable 属于被观察者。
Watchable 接口定义了一个用于将对象注册到 WatchService(监控服务) 并绑定监听事件的方法 register 。
WatchService 用于监听文件目录的变化,同一个 WatchService 对象能够监听多个文件目录。
Path 类 register 方法的第二个参数 events (需要监听的事件)为可变长参数,也就是说我们可以同时监听多种事件。
常用的监听事件有 3 种:
- StandardWatchEventKinds.ENTRY_CREATE :文件创建。
- StandardWatchEventKinds.ENTRY_DELETE : 文件删除。
- StandardWatchEventKinds.ENTRY_MODIFY : 文件修改。
register 方法返回 WatchKey 对象,通过WatchKey 对象可以获取事件的具体信息,
比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。
比如文件目录下是创建、删除还是修改了文件、创建、删除或者修改的文件的具体名称是什么。
WatchService 内部是通过一个 daemon thread(守护线程)采用定期轮询的方式来检测文件的变化,简化后的源码如下所示。
Java IO 模型详解
何为 I/O?
I/O(Input/Outpu) 即输入/输出 。
先从计算机结构的角度来解读一下 I/O。
根据冯·诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。
输入设备(比如键盘)和输出设备(比如显示器)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。
再从应用程序的角度来解读一下 I/O。
根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,
一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,
比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间
我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和响应)。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的
IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。
当应用程序发起 I/O 调用后,会经历两个步骤:
- 内核等待 I/O 设备准备好数据
- 内核将数据从内核空间拷贝到用户空间。
UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
阻塞/非阻塞/同步/异步 IO
这里先介绍下阻塞、非阻塞、同步、异步 I/O 的概念。
先来看看阻塞 I/O,当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,
并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。
并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。
注意,阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程。过程如下图:
知道了阻塞 I/O ,来看看 非阻塞 I/O,非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时
应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。过程如下图:
应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。过程如下图:
注意,这里最后一次 read 调用,获取数据的过程,是一个同步的过程,是需要等待的过程。
这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。
举个例子,如果 socket 设置了 O_NONBLOCK 标志,那么就表示使用的是非阻塞 I/O 的方式访问,而不做任何设置的话,默认是阻塞 I/O。
因此,无论 read 和 send 是阻塞 I/O,还是非阻塞 I/O 都是同步调用。因为在 read 调用时,内核将数据从内核空间拷贝到用户空间的
过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间。
过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间。
而真正的 异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。
当我们发起 aio_read (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程
同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。过程如下图:
同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。过程如下图:
举个你去饭堂吃饭的例子,你好比应用程序,饭堂好比操作系统。
阻塞 I/O 好比,你去饭堂吃饭,但是饭堂的菜还没做好,然后你就一直在那里等啊等,等了好长一段时间终于等到饭堂阿姨把菜端了
出来(数据准备的过程),但是你还得继续等阿姨把菜(内核空间)打到你的饭盒里(用户空间),经历完这两个过程,你才可以离开。
出来(数据准备的过程),但是你还得继续等阿姨把菜(内核空间)打到你的饭盒里(用户空间),经历完这两个过程,你才可以离开。
非阻塞 I/O 好比,你去了饭堂,问阿姨菜做好了没有,阿姨告诉你没,你就离开了,过几十分钟,
你又来饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒里,这个过程你是得等待的。
你又来饭堂问阿姨,阿姨说做好了,于是阿姨帮你把菜打到你的饭盒里,这个过程你是得等待的。
异步 I/O 好比,你让饭堂阿姨将菜做好并把菜打到饭盒里后,把饭盒送到你面前,整个过程你都不需要任何等待。
很明显,异步 I/O 比同步 I/O 性能更好,因为异步 I/O 在「内核数据准备好」和「数据从内核空间拷贝到用户空间」这两个过程都不用等待。
Java 中 3 种常见的 IO 模型
BIO(Blocking I/O)
BIO 属于同步阻塞 IO 模型 。
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统
的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
的 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 。
不单纯是 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 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。
用户线程再发起 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 系统上的性能并没有多少提升。
这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。
最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。
BIO:
- 同步阻塞 IO,一个连接一个线程;
- 发起请求一直阻塞,一般通过线程池改善;
NIO:
- 同步非阻塞 IO,一个请求一个线程;
- 多个连接多路复用一个线程;
AIO:
- 异步非阻塞 IO,一个有效请求一个线程;
- IO 请求立即返回,操作结束后,回调通知;
0 条评论
下一页