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

Java IO

2015-08-12 22:11 465 查看

Java IO

IO问题是整个人机交互的核心问题,因为IO是机器获取和交换信息(包括人机交互和机器与机器交互等)的主要渠道。Java的IO操作主要分为下列几类:

1) 基于字节操作的IO接口:InputStream和OutputStream;

2) 基于字符操作的IO接口:Writer和Reader;

3) 基于磁盘操作的IO接口:File;

4) 基于网络操作的IO接口:Socket;

前面两组讨论的是传输数据的格式(字节或字符),后两组讨论的是传输数据的方式(磁盘IO、网络IO)。

1、 IO接口

1.1、字节IO接口

基于字节IO操作的接口分别对应InputStream和OutputStream,InputStream类层次结构图如下:



输入流根据数据类型和操作方式划分为若干子类,每个子类分别处理不同操作类型。OutputStream类层次结构与InputStream类似,如下:



1.2、字符IO接口

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以IO操作的最终对象都是字节。在应用程序中操作数据一般采用字符的形式,为了操作方便,Java在字节IO的基础上提供了字符IO。

字符IO接口分别对应Reader和Writer类,Reader类主要读字符的接口为intread(char cbuf[],int off,int len),返回成功读到的字符个数,层次结构如下图所示:



类也提供了一个抽象方法用来写字符voidwrite(char cbuf[],int off,int len),其类结构图如下:



Reader和Writer类只是定义了读取或写入字符的方式,也就是怎么写或读;但是并没有规定数据要写到哪里(是磁盘文件还是网络)。

1.3、字节、字符转换

数据持久化或网络传输都是以字节为单位进行的,所以必须要有字符到字节或字节到字符的转换。字节和字符的转换如下所示:



InputStreamReader类是字节到字符的转换桥梁,InputStreamer到Reader的过程要指定编码字符集,否则将采用操作系统的默认字符集(由于Java是跨平台的,最好指定编码字符集,否则容易出现乱码问题)。StreamDecoder是完成字节到字符解码的实现类。



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

2、 磁盘IO机制

读写文件都是调用操作系统的API(read、write接口)完成的,因为磁盘设备是由操作系统统一管理的,应用程序要访问物理设备只能通过系统调用的方式来工作。发生系统调用时就可能存在内核地址空间和用户地址空间切换的问题,进而导致数据从内核空间向用户空间复制的问题。

如果遇到非常耗时的操作,数据从磁盘复制到内核空间,再从内核空间复制到用户空间,将会非常缓慢。操作系统为了加速IO的访问,在内核空间采用缓存机制将读取的文件按照一定的组织方式进行缓存,如果用户程序访问的是同一段磁盘地址空间的数据,直接将内核缓存中的数据返回给用户程序。

2.1、标准访问文件方式

标准访问文件方式就是当应用程序调用read()接口时,OS会检查内核的高速缓存中有没有需要的数据,如果缓存中存在数据,直接将缓存中的数据返回;如果没有,则从磁盘中读取,然后在内核中缓存并返回给用户程序。

当写入时,用户程序调用write()接口将数据从用户地址空间复制到内核地址空间的缓存中。这时对用户程序来说写操作已完成,至于什么时候再写到磁盘中由OS决定。



2.2、直接IO方式

所谓直接IO方式就是应用程序直接访问磁盘数据,而不经过OS内核数据缓冲区,这样可以减少从内核缓冲区到用户程序缓存的数据复制。直接IO也存在缺点,如果访问的数据不在应用程序缓存中,则每次都直接从磁盘加载,增加IO响应时间。通常将直接IO和异步IO结合使用,并在应用层增加缓存机制,会得到较好的性能。



2.3、同步访问文件方式

同步访问文件方式比较容易理解,就是数据的读取和写入都是同步操作。它与标准访问文件方式的不同时,只有当数据被成功写回磁盘时才返回给应用程序成功的标志。这种同步访问文件方式性能比较差,只有在一些对数据安全性要求比较高的场景中使用,而且通常这种操作方式的硬件都是定制的。



2.4、异步访问文件方式

异步访问文件方式就是当访问数据的线程发出请求后,线程接着处理其他事情,而不是阻塞等待;当请求的数据返回后请求线程继续处理下面的操作。这种访问文件的方式可以明显提高应用程序的效率,但并不会改变访问文件的效率。



2.5、内存映射方式

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



2.5、Java访问磁盘文件

数据在磁盘中的访问单位就是文件,也就是说应用程序只能通过文件来操作磁盘上的数据。

在Java中,File是用来处理文件和文件夹,如新建、删除等操作;而并不能来读写文件,File只是指定了读写操作的目标位置,至于如何读取或写入文件,需要相关的输入或输出流来实现。Java读取一段文本文件的过程如下



此外,File类的实例是不可变的;也就是说,一旦创建File对象,File对象表示的抽象路径将不会改变。由于File既可以表示文件,也可以表示目录,在实际应用中可能会有过滤某些文件、文件夹的操作,如下

l 过滤文件

在遍历某个文件夹下所有的文件时,可以创建FileNameFilter实例来对特定的文件进行过滤。

FileNameFilter是个接口,需要提供此接口的实现类,重写accept方法来对特定的文件进行过滤。

l 过滤文件夹

在遍历某个文件夹的文件时,该文件夹可能存在子文件夹,若要实现对文件夹的过滤,可提供FileFilter实现类。

3、 网络IO机制

Socket描述了计算机之间完成通信的一种抽象功能,Socket可以有多种实现,如基于UDP的套接字和基于TCP/IP的流套接字。大部分情况下,我们使用的都是基于TCP/IP的流套接字,它是一种稳定的通信协议。

采用基于TCP/IP的流套接字进行通信,首先需要建立连接。当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStream和OutputStream实例,并通过这两个对象来交换数据,以实现数据的传输。

网络IO都是基于字节流的,当创建Socket对象时,操作系统会为Socket分配一定大小的缓冲区,数据的读取和写入都是通过缓冲区来完成的。写入端将数据写到OutputStream对应的SendQ队列中,当队列填满时,数据被转移到另一端InputStream的RecvQ队列中,如果RecvQ已满,那么OutputStream的write方法将阻塞直到RecvQ队列有足够的空间容纳SendQ发送的数据。缓冲区的大小以及写入端的速度和读取端的速度都会影响数据传输率,由于可能会发生阻塞,如果通信两端同时传送数据可能会产生死锁的情况。

3.1、BIO

标准IO的各种流是阻塞的(阻塞IO),也就是说当一个线程调用read或write方法时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。在读取或写入期间该线程不能在干其他任何事情了。



BIO(BlockingIO)即阻塞IO,不管是磁盘IO还是网络IO,数据在写入OutputStream或者从InputStream读取数据时都有可能会阻塞,一旦阻塞,线程将失去CPU控制权。

当调用ServerSocket对象的accept方法时,将会一直阻塞到有客户端连接才会返回,每个客户端连接请求,服务器端都回启动一个线程去处理请求。若存在大规模访问量,虽然可以采用线程池技术,为每个请求对应一个处理线程,但线程的创建和回收也会消耗大量的资源。此外线程数量并不是越多越好,在一定范围内增加线程数量,可以提高应用的性能;当线程数量超过一定值后,应用程序的性能将会下降,甚至瘫痪。

此外,如果应用需要保持大量的HTTP长连接(如网页即时聊天工具),若每个长连接对应一个线程,会是怎样的开销。

3.2、NIO

NIO(NewIO)也称非阻塞IO。在标准IO中,所有的操作都是基于流的;而NIO是面向通道和缓冲区的。标准IO基于流是指每次从流中读取一个或多个字节,找到读取所有的字节;此外它不能移动流中的数据,如果需要前后移动从流中读取数据,需要将它缓存到一个缓冲区中。NIO将数据读取到一个缓冲区中,需要时可以在缓冲区中前后移动。

此外,NIO是非阻塞模式的,当一个线程从某个通道发送请求读取数据时,它仅能得到目前可用的数据;如果目前没有可用的数据,就什么都不会获取;但并不会阻塞该线程,所以直到数据变的可读之前,该线程可以继续做其他事情。当一个线程请求写数据到某通道时,不需要等待完全写入,该线程同时可以处理其他事情。线程通常将非阻塞IO的空闲时间用在其他通道上执行IO操作,所以一个单独的线程就可以管理多个输入和输出通道。

3.3、总结

在实际应用中,需要根据具体的需求和复杂度来确定采用哪种IO操作方式。并不是说NIO就比保准IO好,各有各的实际用途。

NIO虽然可以只使用单个线程来管理多个通道(网络连接或文件),但其解析数据比保准IO更复杂。如果需要同时管理的连接数达到成千上万个,而且每个连接只是发送少量的数据,如聊天服务器,采用NIO实现是一个比较好的选择。如果连接数较少,且每次需要传输大量的数据,标注IO将是比较好的选择。

4、 总结

在Java中IO可按照多个方面进行分类,如下

l 方向:IO流分为输入和输出流;

l 类型:字节流和字符流

l 操作方式:节点流、过滤流

l 转换流

4.1、字节流

字节流在读写文件时,以字节为单位,适合处理二进制文件,如图片、压缩包等。在Java中字节流的基类为InputStream和OutputStream,常用的类(以输入流为例)有FileInputStream、DataInputStream、ObjectInputStream等。

4.1.1、DataInputStream

数据输入流允许程序以与机器无关的方式从底层输入流中读取Java基本数据类型,如可以从二进制文件中读取boolean、byte、int等基本数据类型。

为什么需要DataInputStream和DataOutputStream来进行基本数据类型的读写操作呢?我们知道对于整形数123456,若以字符流保存需要6个字节;而采用字节流,整形只需要4个字节即可,可有效节省空间。

4.1.2、ObjectInputStream

ObjectInputStream对使用ObjectOutputStream写入的基本数据和对象进行反序列化。

总结:在Java中字节流通常以XXXInputStream、XXXOutputStream来命名。

4.2、字符流

字符流以字符为基本单位进行读写数据,基类为Reader和Writer。其直接子类有BufferedReader、StringReader、InputStreamReader,FileReader。

总结:

l 输入字符流中BufferedReader比较常用,而字符输入流中,PrintWriter则比较常用,PrintWriter可以格式化输出的数据,如System.out即为PrintWriter实例;

l 在Java中字符流通常以XXXReader、XXXWriter来命名。

4.3、节点流

可直接创建的流,并不需要依赖其他流对象,如FileInputStream。

4.4、过滤流

在Java中,过滤流是采用装饰模式,基于节点流或其他过滤器而创建的功能增强的流,如输入输出缓冲流等(在节点流的基础上增加缓冲区,提高效率)。因此过滤流是不能直接创建实例的,需要依赖其他流实例,以增强流的功能。

4.5、转换流

转换流是将字节流转换为字符流的流,在字符流的基础上提供增强功能。转换流也是过滤器,依赖字节流而存在。如FileReader为InputStreamReader的子类,而InputStreamReader为转换流,所以FileReader也为转换流。

System.in为标准字节流,不方便操作,所以在使用System.in时往往将其装转换为字符流,再转换为BufferedReader来操作。

在Java中,InputStreamReader,OutputStreamWriter来实现字节流和字符流的转换,如下



参考资料:

1:http://www.ibm.com/developerworks/cn/linux/l-cn-directio/

2:深入分析Java Web技术内幕/许令波著
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: