深入分析Java I/O的工作机制

数据在网络中随处流动,在这个流动的过程中都涉及到I/O问题,可以说大部分Web系统的瓶颈都是I/O瓶颈

Java 的I/O类库的基本架构

Java的I/O操作类都在包java.io下,大概有80多个类,基本上可以分为以下4类

  • 基于字节操作的I/O接口:InputStreamOutputStream
  • 基于字符操作的I/O接口:WriterReader
  • 基于磁盘的I/O接口:File
  • 基于网络的I/O接口:Socketjava.net

前两组主要是传输数据的数据格式,后两组主要是传输数据的方式。

I/O的核心问题要么是数据格式影响I/O操作,要么是传输方式影响I/O操作,也就是将什么样的数据写到什么地方的问题

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

基于字节的I/O操作接口分别是:InputStreamOutputStream

这些接口

  1. 操作数据的方式可以组合使用(接口之间互相包装)
  2. 必须指定流最终写到什么地方,要么写到磁盘,要么写到网络。实际上写网络也是在写文件,只不过是让操作系统再把数据传送到其他地方而不是本地磁盘

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

不管网络传输还是磁盘,最小的存储单位都是字节,而不是字符,但是为什么还有操作字符的接口?这是因为程序中常见的数据都是字符形式,为了方便当然需要提供一个直接写字符的I/O的接口。从字符到字节必须经过编码转换,而且过程中经常伴随着乱码

基于字符的I/O操作接口分别是:ReaderWriter

字节与字符的转化接口

InputStreamReader类是从字节到字符的转化桥梁,从InputStreamReader的过程需要指定字符集,否则使用的就是平台默认的,很可能会出现乱码。StreamDecoder正是完成字节到字符的解码的实现类

OutputStreamWriter类完成字符到字节的编码过程,由StreamEncoder完成编码过程

磁盘I/O工作机制

读取和写入文件I/O操作都是调用操作系统提供的接口,因为磁盘是由操作系统管理的,应用程序要访问物理设备只能通过系统调用方式来工作。只要是系统调用就可能存在内核空间地址和用户空间地址切换的问题。这是操作系统为保护本身的运行安全,而将内核使用内存空间和用户运行的内存空间进行隔离造成的。但是这样可以保证内核安全,但是也必然存在数据可能需要从内核空间先用户空间复制的问题。

如果遇到非常耗时的操作,数据从磁盘复制到内核空间,然后又从内核空间复制到用户空间,将会非常缓慢。这时操作系统为了加速I/O操作,在内核空间存在缓存机制,也就是将从磁盘读取到的文件按照一定的组织方式进行缓存,如果用户程序访问的是同一段磁盘空间数据,那么操作系统直接从缓存中取出

几种访问文件的方式

  1. 标准访问文件的方式

标准I/O就是应用程序访问文件时,操作系统检查在内核的高速缓存中有没有需要的数据,如果已经缓存了,就直接从缓存中返回,如果没有则从磁盘中读取,然后缓存到操作系统的缓存中

写入时数据从用户地址空间复制到内核地址空间的缓存中,这时对于程序来说写操作已经完成,至于什么时候再写入磁盘由操作系统决定,除非显式调用sync同步命令

  1. 直接I/O的方式

所谓直接I/O就是应用程序直接访问磁盘数据,而不经过操作系统内核数据缓冲区,这样做的目的就是为了减少以吃从内核到用户的数据复制。数据库管理系统通常采用这样的方式

直接I/O也有负面影响,如果访问的数据不在应用程序缓存中,那么每次都会直接从磁盘加载,这种直接加载会非常缓慢。通常直接I/O和异步I/O相结合会得到比较好的性能

  1. 同步访问文件的方式

同步访问即数据的读取和写入都是同步操作,与标准I/O不同的是,只有当数据被写入磁盘之后才返回给应用程序成功的标志

这种访问模式性能一般较差,只有对一些数据安全性要求比较高的场景才会使用

  1. 异步访问文件的方式

异步访问文件的方式就是当访问数据的线程发出请求后,线程接着去处理其他的事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作。这种方式可以明显提升应用程序的效率,但是不会改变访问文件的效率,因为访问文件的动作没有改变,访问一个文件该用多少时间还是多少

  1. 内存映射的方式

内存映射的方式就是将操作系统的内存中的某块区域与磁盘中的文件关联起来,当要访问磁盘文件中的数据,就转为访问内存中的数据,这种方式同样是减少数据从内存空间缓存到用户空间缓存的数据复制

Java访问磁盘文件

数据在磁盘中唯一最小的描述就是文件,也就是说上层应用程序只能通过文件操作磁盘上的数据。在Java中通常的File并不代表一个真实存在的文件对象,当你指定一个路径时,它就会返回一个代表这个路径的虚拟对象,有可能是一个文件,也有可能是一个包含多个文件的目录,而且这个文件对象也有可能并不真实存在。为什么要这样设计?因为在大多数情况下,我们并不关心这个文件是否真实存在,而是关心对这个文件如何操作

何时会真正检查文件存不存在?就是在真实要操作这个文件时。例如FileInputStream类是一个操作文件的接口,在创建这个对象时会创建出FileDescriptor对象,FileDescriptor对象才是真正代表一个存在的文件描述。通过FileInputStream.getFD()方法获取真正操作的与底层操作系统相关的描述,例如可以通过FileDescriptor.sync()方法将操作系统中的数据强制刷新到物理磁盘

至于如何从磁盘上读取一段数据,操作系统会帮我们完成。操作操作系统是如何将数据持久化到磁盘及如何建立数据结构的,需要根据当前操作系统使用的何种文件系统(Windows:NTFS, FAT32)来回答

Java序列化技术

序列化(ObjectOutputStream)就是将一个对象转化为一串二进制字节数组,可以保存或转移这些数据。需要序列化就需要实现java.io.Serializable接口。反序列化(ObjectInputStream)就是相反的过程。反序列化必须有原始类作为模板,才能将这个对象还原,也就是说序列化的数据不像class文件那样保存类的完整的结构信息

序列化注意事项:

  • 当父类实现java.io.Serializable接口,所有的子类都可以序列化
  • 子类都可以实现Serializable接口,父类没有。父类中的属性不能序列化(不报错,数据会丢失),但是子类的属性仍能正常序列化
  • 如果序列化的属性也是对象,那么这个对象也要实现Serializable接口
  • 在反序列化时,如果对象的属性有修改或删除,则修改的部分属性会丢失,但不会报错
  • 在反序列化时,如果serialVersionUID被修改,则反序列化会失败

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

网络/O工作机制

当客户端要与服务端通信时,客户端首先需要创建一个Socket实例,操作系统将为这个Socket实例分配一个没有使用过的本地端口号,并创建一个包含本地地址,远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直至这个连接关闭。在创建Socket实例的构造函数正确返回之前,将要进行TCP的3次握手,握手完成之后Socket实例将被创建,否则抛出IOException

与之对应的服务端将创建一个ServerSocket实例,创建比较简单,只要指定的端口号没被占用一般都会创建成功。操作系统会为ServerSocket创建一个底层的数据结构,这个结构包括指定的监听的端口号和监听地址通配符(一般是* 即监听所有地址)。之后调用accept()方法进入阻塞状态,等待客户端请求。当一个新的请求请求到来之后,将会为这个请求创建一个Socket数据结构,包括请求源端口和地址,注意这时的Socket实例会关联到ServerSocket实例的一个未完成连接的列表中,当TCP三次握手之后,服务端的这个Socket实例才会返回,并将从未完成连接列表中移入已完成列表

当连接已经建立,服务端和客户端都会拥有各自的一个Socket实例,每一个Socket实例都包含一个InputStreamOutputStream,并通过这两个对象来交换数据

I/O交互方式

  1. 同步和异步

所谓同步就是一个任务完成需要另一个任务时,只有等待被依赖的任务完成后,依赖的任务才能完成,整个一个一种可靠的任务序列。要么同时成功,要么都失败,两个任务的状态可以保持一致。而异步不需要等待被依赖的任务完成,只是通知被依赖的任务,依赖的任务也立即执行,只要自己完成整个任务就算完成了,至于被依赖的任务是否最终完成,依赖它的任务无法确定,所以它是不可靠的任务序列

同步可以保证程序的可靠性,异步可以提升程序的性能,必须在可靠性和性能之间保持平衡,没有完美的解决方案。

  1. 阻塞和非阻塞

阻塞和非阻塞是对于CPU的消耗来说的,阻塞就是CPU停下来等待一个慢的操作完成之后,CPU才接着完成其他工作。非阻塞就是在这个慢操作时,CPU可以去做其他工作,等这个慢操作完成时,CPU再接着后续操作。表面上非阻塞的方式可明显提高CPU的利用率,但是也有另一个否关,就是系统线程切换增加。增加的CPU使用时间能不能补偿系统的切换成本需要好好评估

这两种方式互相组合后有4种:同步阻塞,异步阻塞,同步非阻塞,异步非阻塞

虽然异步和非阻塞能够提升I/O的性能,但是也会带来一些额外的性能成本,例如:增加线程数量从而增加CPU的消耗,同时也会导致程序设计的复杂度上升。如果设计的不合理反而会导致性能下降,在实际设计时要根据应用场景综合评估

posted @ 2020-10-04 18:19  OverZeal  阅读(227)  评论(0编辑  收藏  举报