您的位置:首页 > 编程语言 > Java开发

Java I/O的工作机制1

2016-09-13 19:13 127 查看
Java I/O类库的基本架构

I/O问题是任何编程语言都无法回避的问题,可以说I/O问题是整个人机交互的核心问题,因为I/O是机器获取和交换信息的主要渠道。在当今数据大爆炸的时代,I/O问题尤其突出,很容易成为一个性能瓶颈。正因为如此,Java在I/O上也一直在做持续的优化,如从1.4版开始引入了NIO,提升了I/O的性能。

Java的I/O操作类在包java.io下,大概有将近80个类,这些类大概可以分成如下4组:
1)基于字节操作的I/O接口:InputStream和OutputStream
2)基于字符操作的I/O接口:Writer和Reader
3)基于磁盘操作的I/O接口:File
4)基于网络操作的I/O接口:Socket
前两组主要是传输数据的数据格式,后两组主要是传输数据的方式,虽然Socket类并不在java.io包下,但这里却仍然把它们划分到了一起,因为I/O的核心问题要么是数据格式影响I/O操作,要么是传输方式影响I/O操作,也就是将什么样的数据写到什么地方的问题。I/O只是人与机器或机器与机器交互的手段,除了它们能够完成这个交互功能外,我们关注的就是如何提高它的运行效率了,而数据格式和传输方式是影响效率最关键的因素。

基于字节的I/O操作接口:





注意:在使用流的时候,必须要指定流最终写到什么地方,要么写到磁盘,要么写到网络中。

基于字符的I/O操作接口:

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以I/O操作的都是字节不是字符。但我们的程序中操作的数据通常都是字符形式的,为了操作方便就提供了一个直接写字符的I/O接口。从字符到字节必须要经过编码转换,而这个编码又非常耗时,且还常会出现乱码问题,所以I/O的编码问题经常让人头疼。





Writer类提供了一个抽象方法write(char cbuf[], int off, int len),Reader类的操作接口是 int read(char cbuf[], int off, int len),返回读到的n个字节数。不管是Writer还是Reader,它们都只定义了读取或写入的数据字符的方式,并没有规定数据要写到哪里。

字节与字符的转化接口:

数据持久化或网络传输都是以字节进行的,所以必须要有从字符到字节或字节到字符的转化。



InputStreamReader类是从字节到字符的转化桥梁,从InputStream到Reader的过程要指定编码字符集,否则将采用操作系统默认的字符集,很可能会出现乱码问题。StreamDecoder正是完成从字节到字符的解码的实现类。



通过OutputStreamReader类完成了从字符到字节的编码过程,由StreamEncoder完成编码过程。

磁盘I/O工作机制:

在读取和写入文件I/O操作都调用操作系统提供的接口,因为磁盘设备是由操作系统管理的,应用程序要访问物理设备只能通过系统调用的方式来工作。读和写分别对应read()和write()两个系统调用。而只要是系统调用就可能存在内核空间地址和用户空间地址切换的问题,这是操作系统为了保护系统本身的运行安全,将内核程序运行使用的内存空间和用户程序运行使用内存空间进行隔离造成的。这样可以保护内核程序运行的安全。虽然如此,但也必然存在数据可能需要从内核空间向用户空间复制的问题。
如果遇到非常耗时的操作,如磁盘I/O,数据从磁盘复制到内核空间,然后又从内核空间复制到用户空间,将会非常缓慢。这时操作系统为了加速I/O访问,在内核空间使用缓存机制,即将从磁盘读取的文件按照一定的组织方式进行缓存,如果用户程序访问的是同一段磁盘地址的空间数据,那么操作系统将从内核缓存中直接取出返回给用户程序,这样可以减小I/O的响应时间。

几种访问文件的方式:

1.标准访问文件的方式

当应用程序调用read()接口时,操作系统检查在内核的高速缓存中有没有需要的数据,如果有,则从缓存中返回,如果没有,则从磁盘中读取,然后缓存在操作系统的缓存中。在调用write()接口时,应用程序将数据从用户地址空间复制到内核地址空间的缓存中。这时对于用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显式地调用sync同步命令。



2.直接I/O的方式

这种方式是指,应用程序直接访问磁盘数据,而不经过操作系统内核数据缓冲区,这样做是为了减少一次从内核空间到用户空间的数据复制。这种访问文件的方式通常是在对数据的缓存管理有应用程序实现的数据库管理系统中。在数据库管理系统中,系统明确地知道应该缓存哪些数据,应该失效哪些数据,还可以对一些热点数据做预加载,提前将热点数据加载到内存,可以加速数据的访问效率。在这些情况下,操作系统并不知道哪些是热点数据,哪些数据可能只访问一次就不会再访问了,操作系统只是简单地缓存最近一次从磁盘读取的数据,所以它做不到这样的数据缓存。

但直接I/O也有负面影响,如果访问的数据不在应用程序缓存中,那么每次数据都要从磁盘进行加载,这种直接加载会很慢。通常直接I/O与异步I/O结合使用会很好。



3.同步访问文件的方式



4.异步访问文件的方式



5.内存映射方式

内存映射的方式是指操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据是,转换为访问文件的某一段数据。这也是为了减少数据从内核空间缓存到用户空间缓存的复制操作,因为这两个空间的数据是共享的。



Java访问磁盘文件

上面是基本的Java I/O的操作接口,这些接口主要定义了如何操作数据,和操作数据结构的字节和字符的两种方式。还有一个关键问题就是数据写到何处。其中一个主要方式就是将数据持久化到物理磁盘。数据在磁盘中的唯一最小描述就是文件,即上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的最小单元。

在Java中,File对象并不代表一个真实存在的文件对象,当你指定一个路径描述符时,它就会返回一个代表这个路径的虚拟对象,这可能是一个文件,也可能是一个目录。这样设计,是因为通常我们并不关心这个文件是否真实存在,而是关心对这个文件到底如何操作。只有在真正读取文件时,才会检查这个文件存不存在。

例如,FileInputStream类都是操作一个文件的接口,注意到在创建一个FileInputStream对象时会创建一个FileDescriptor对象,其实这个对象就是真正代表一个存在的文件对象的描述。当我们在操作一个文件对象时可以通过getFD()方法获取真正操作的与底层操作系统相关联的文件描述。例如,可以调用FileDescriptor.sync()方法将操作系统缓存中的数据强制刷新到物理磁盘中。



Java序列化技术

Java序列化就是将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。需要持久化,对象必须继承java.io.Serializable接口。反序列化则是相反的过程,将这个字节数组再重新构造成对象。

虽然Java的序列化能够保证对象状态的持久保存,但遇到一些对象结构复杂的情况还是比较难处理的,如:

1.当父类继承Serializable接口时,所有子类都可以被序列化

2.子类实现了Serializable接口,父类没有,父类中的属性不能序列化(不报错,数据会丢失),但子类的属性仍能正确序列化

3.如果序列化的属性是对象,则这个对象必须实现Serializable接口,否则会报错

4.在反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错

5.在反序列化时,如果serialVersionUID被修改,则反序列化时会失败

在纯Java环境下,Java序列化能够很好地工作,但是在多语言环境下,用Java序列化存储后,很难用其他语言还原出结果。在这种情况下,还是要尽量存储通用的数据结构,如JSON或者XML结构数据。

网络I/O工作机制

TCP状态转化:在进行socket通信之前,先看看如何建立和关闭一个TCP链接。



1.CLOSED: 起始点,在超时或者链接关闭时进入此状态

2.LISTEN: Server端在等待连接时的状态,Server端为此要调用Socket、bind、listen函数,就能进入此状态。这称为应用程序被动打开(等待客户端来连接)

3.SYN-SENT: 客户端发起链接,发送SYN给服务器端。如果服务器端不能连接,则直接进入CLOSED状态。

4.SYN-RCVD: 与3对应,服务器端接受客户端的SYN请求,服务器端由LISTEN状态进入此状态。同时服务器端要回应一个ACK,发送一个SYN给客户端;另外一种情况是,客户端在发起SYN的同时接收到服务器端的SYN请求,客户端会由SYN-SENT转换到SYN-RCVD状态。

5.ESTABLISHED: 服务器端和客户端在完成3次握手后进入状态,说明已经可以开始传输数据了。

6.FIN-WAIT-1: 主动关闭的一方,由状态5进入此状态。具体动作是发送FIN给对方。

7.FIN-WAIT-2: 主动关闭的一方,接受到对方的FIN ACK,进入此状态。由此不能再接受对方的数据,但能向对方发送数据。

8.CLOSE-WAIT: 接受到FIN后,被动关闭的一方进入此状态。具体的动作是在接受到FIN的同时发送ACK。

9.LAST-ACK: 被动关闭的一方,发起关闭请求,由状态8进入此状态。具体动作是发送FIN给对方,同时在接收到ACK时进入CLOSED状态。

10.CLOSING: 两边同时发起关闭请求时,会由FIN-WAIT-1进入此状态。剧吐动作是接收到FIN请求,同时响应一个ACK。

11.TIME-WAIT: 这个状态比较复杂,也是最常见的一个连接状态,有3个状态可以转化为此状态。

1)由FIN-WAIT-2转换到TIME-WAIT,具体情况是:在双方不同时发起FIN的情况下,主动关闭的一方在完成自身发起的关闭请求后,接收到被动关闭一方的FIN后

进入的状态

2)由CLOSING转换到TIME-WAIT,具体情况是:在双方同时发起关闭,都做了发起FIN的请求,同时接收到了FIN并做了ACK的情况下,这时就由CLOSING状态进

入TIME-WAIT状态

3)由FIN-WAIT-1转换到TIME-WAIT,具体情况是:同时接收到FIN(对方发起)和ACK(本身发起的FIN回应),它与CLOSING转换到TIME-WAIT的区别在于本

身发起的FIN回应的ACK先于对方的FIN请求到达,而由CLOSING转换到TIME-WAIT则是FIN先到达

搞清楚TCP连接的几种状态转换对我们调试网络程序是非常有帮助的。例如,在压测一个网络程序时可能遇到CPU、网卡、带宽等都不是瓶颈,但是性能就是上不去的情况,如果观察一下网络连接情况,看看当前的网络连接都处于什么状态,可能就会发现由于网络连接的并发数不够导致连接都处于TIME-WAIT状态,这时就要做TCP网络参数调优了。

影响网络传输的因素:

将一份数据从一个地方正确地传输到另一个地方所需要的时间被称为响应时间。影响这个响应时间的因素有很多。如:

网络带宽:所谓带宽就是一条物理链路在1s内能够传输的最大比特数(是比特数,而不是字节数,b/s)。

传输距离:即数据在光纤中要走的距离。

TCP拥塞控制:TCP传输是一个“停-等-停-等”的协议,传输方和接受方的步调要一致,要达到步调一致就要通过拥塞控制来调节。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: