本文将介绍一些与IO有关的java api以及read/write接口调用的过程分析,主要内容有以下几点:
- 文件File,文件句柄FileDescriptor,RandomAccessFile,FileChannel
- 输入输出流,包括输入输出字节流,输入输出字符流,字符流与字节流的转换
- 输入输出流的缓存包装器BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter
- Java进程中IO read/write 请求的全过程
(1)用来表示开放文件、开放套接字等的句柄
(2)持有标准输入标出流(FileDescriptor.in / out / error )的句柄
(3)sync()方法将操作系统缓存中的数据强制刷新到物理磁盘中。
3 RandomAccessFile
属于传统的IO类,持有FileDescriptor与FileChannel,封装了一些读写功能,如访问指定位置的内容,按基本的数据类型读写文件等.
4 FileChannel
(1)FileChannel属于NIO的类,是一个连接到文件的通道,可以通过文件通道读写文件,提供了很多有用的读写控制功能,如权限,定位,锁,裁剪,强制刷到磁盘等等,它与RandomAccessFile最明显的区别在于,其使用ByteBuffer进行读写,而RandomAccessFile使用字节数组
(2)FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下;
(3)无法直接打开一个FileChannel,需要通过使用InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。
二 输入输出字节流,输入输出字符流,字符流与字节流的转换
1 基于字节的InputStream OutputStream
是基于字节操作的 I/O 接口
InputStream需要指定一个字节流的来源,如File,调用read方法读取来源的字节数据到指定的地方(或直接返回,如ObjectOutputStream.readObject());
OutputStream需要指定一个字节流的目的地,调用write方法将指定的字节数据写到目的地;
例如:
ByteArrayInputStream字节流来源是其内部的字节数组;ByteArrayOutputStream的字节流目的地也是其内部的字节数组;
ObjectInputStream 对以前使用 ObjectOutputStream 写入的基本数据和对象进行反序列化;
ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。
ObjectInputStream的字节流来源可以是FileInputStream; ObjectOutputStream的字节流目的地可以是FileOutputStream;
2 FileInputStream和FileOutputStream
(1)用于封装对文件的读写(FileDescriptor.fd=-1且handle != -1)或对标准输入标出流(FileDescriptor.in / out / error )的读写(分别对应FileDescriptor.fd= 0 / 1 / 2)
(2)在其构造方法中,如果传入的不是FD对象,那么表示封装对操作系统文件的读写,会有个open()的系统调用以打开文件进行读写
(3)★这个open()的系统调用会促使系统在内核构造一个对应文件的结构体,并返回一个long型值代表的句柄赋予FileDescriptor.handle
3 基于字符操作的 I/O 接口:Writer 和 Reader (StreamEncoder StreamDecoder)
FileReader与 FileWriter:
分别基于InputStreamReader和OutputStreamWriter,他们的来源和目的地分别是InputStream和OutputStream,而他们俩又进一步依赖于StreamDecoder与StreamEncoder,
FileReader的来源是FileInputStream,FileWriter的来源是FileOutputStream.(
FileReader与 FileWriter使用的编码是系统默认编码)
StreamDecoder担当将来源的字节解码成指定的编码的字符串(内存中以unicode表示,格式为utf-16)
StreamEncoder担当将指定的字符串以指定的编码写入目的地
所以说,★字符流的操作是在jvm层次而不是在本地的
4 字符流与字节流的转换.............................待整理■
三 输入输出流的缓存包装
1 BufferedInputStream 与 BufferedOutputStream
(1)IO读写的一些原则:尽量一次性读写一段数据,而不是以字节为单位的多次读写,在jvm层面,这样可以减少对本地方法调用的次数,从而导致在系统层面,可减少内存与外存的IO的交互次数以及用户区与内核区数据的传递次数,因为现今的计算机都支持DMA块读写,系统调用也是可以一次性读写一段数据的.
(2)BufferedInputStream封装了一个缓冲区,使用户在操作IO时不用自己设计和维护缓冲区.外界对BufferedInputStream的读取操作实际上是在缓冲区上进行,如果读取的数据超过了缓冲区的范围,那么BufferedInputStream负责重新从原始输入流中载入下一截数据填充缓冲区,然后外界继续通过缓冲区进行数据读取。
(3)BufferedOutputStream封装了一个缓冲区,使用户在操作IO时不用自己设计和维护缓冲区.当使用write()方法写入数据时实际上会先将数据写到buf中,当buf已满,或者调用flush()时才会调用给定的OutputStream对象的write()方法,将buf数据写到目的地,而不是每次都对目的地作写入的动作。
2 BufferedReader,BufferedWriter.............................待整理■
四 Java进程中IO read/write 请求的全过程
1, read整体过程
①-> 生成FileInputStream或RandomAccessFile,构造方法中执行系统调用open,内核创建一个结构体(代表对应的文件或外部设备的信息,包含有存放数据的缓冲区) 并返回句柄值赋予FileDescriptor.handle.
②-> 调用java IO流的read()
③-> 调用本地方法的read()
④-> 本地方法执行系统调用read()
⑤-> 系统通过驱动程序的读指令向硬件发送IO指令将外存的数据搬进内核缓冲区(底层是由中断或DMA方式实现,注意系统可能会预先从外存中读进一批数据,减少了IO的内外交互次数)
⑥-> 系统再把内核缓冲区的数据拷备到jvm进程缓存区
2, read过程分析
(1) 在①②③④步顺序执行后,java中对应请求io的线程,即调用read()方法的线程将被阻塞,以等待读就绪状态及IO的完成,即等待外存中的数据搬进内核缓冲区
(2) 当⑤步执行后,为读就绪状态
(3)读就绪状态后,执行⑥,IO过程到此完成,★线程不再阻塞,
3,write整体过程
①-> 生成FileOutputStream或RandomAccessFile,构造方法中执行系统调用open,内核创建一个结构体(代表对应的文件或外部设备的信息,包含有存放数据的缓冲区) 并返回句柄值赋予FileDescriptor.handle.
②-> 调用java IO流的write()
③-> 调用本地方法的write()
⑤-> 本地方法执行系统调用write()
⑥-> 系统把jvm进程缓存区的数据拷备到内核缓冲区
⑦-> 系统通过驱动程序的写指令向硬件发送IO指令将内核缓冲区的数据搬到外存(底层是由中断或DMA方式实现)
4 write过程分析
(1)在①②③⑤步顺序执行后,IO请求线程被阻塞,等待IO写就绪与IO的完成
(2)在⑥步可以执行的时候,说明已经写就绪了,于是系统把用户进程缓存区的数据拷备到内核缓冲区
(3)当第⑥步执行完后,真正的IO过程到此完成,★线程不再阻塞.
(4)可以使用对象持有的FileChannel实例的force(boolean metaData)方法强制执行第⑦步,如果不使用强制,那么将内核缓冲区的数据刷到外存的时机依赖于系统的具体实现. (FileDescriptor.sync()方法可以把文件内容刷到外存,但是,没有boolean metaData参数,不能将文件元数据刷到外存 )