Java的I/O操作类在包java.io下,这些类大概可以分为如下四组。
1.基于字节操作的I/O接口:InputStream和OutputStream
2.基于字符操作的I/O接口:Writer和Reader
3.基于磁盘操作的I/O接口:File
4.基于网络操作的I/O接口:Socket
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以I/O操作的都是字节而不是字符。
字符到字节必须要经过编码转换,而且还会经常出现乱码问题,所以I/O的编码问题经常是让人头疼的问题。
另外,数据持久化或网络传输都是以字节进行的,所以必须要有字符到字节或字节到字符的转化。
几种访问文件的方式
1.标准访问文件方式
标准访问文件方式就是当应用程序调用read()接口时,操作系统检查内核的高速缓存中有没有需要的数据,如果有,那么就直接从缓存中返回,如果没有,从磁盘中读取,然后缓存在操作系统的缓存中。
2.直接I/O方式
所谓直接I/O方式就是应用程序直接访问磁盘数据,而不经过操作系统内核数据缓冲区,这样做的目的就是减少一次从内核缓冲区到用户程序缓存的数据复制。
3.同步访问文件方式
数据的读取和写入都是同步操作的,它与标准访问文件访问方式不同的是,只有当数据被成功写到磁盘时才返回应用程序成功标志。
4.异步访问文件方式
当访问数据的线程发出请求之后,线程会接着去处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作。
5.内存映射方式
操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中一段数据时,转换为访问文件的某一段数据。
Java访问磁盘文件
数据在磁盘中的唯一最小描述就是文件,也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的最小单元。
Java序列化技术
Java序列化就是将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。我们知道反序列化时,必须有原始类作为模板,才能将这个对象还原。
虽然Java的序列化能够保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是比较难处理的,下面是一些复杂对象情况的总结。
-
当父类继承Serializable接口,所有子类都可以被序列化。
-
子类实现了Serializable接口,父类没有,父类中的属性不能序列化,但是子类中属性仍能正确序列化。
-
如果序列化的属性是对象,这个对象也必须实现Serializable接口,否则会报错。
-
在反序列化时,如果对象的属性有修改或删减,修改的部分属性会丢失,但不会报错。
-
在反序列化时,如果serialVersionUID被修改,那么反序列化时会失败。
网络I/O工作机制
TCP拥塞控制:我们知道TCP传输是一个“停-等-停-等”协议,传输方和接收方的步调要一致,要达到这个步调一致就要通过拥塞控制来调节。TCP在传输是会设定一个窗口(BDP,Bandwidth Delay Product),这个窗口的大小是由带宽和RTT(数据在两端来回的时间)决定的。计算的公式是带宽(b/s)*RTT(s)。
NIO的工作方式
BIO即阻塞I/O,不管是磁盘I/O还是网络I/O,数据在写入OutputStream或者从InputStream读取时都有可能会阻塞,一旦有阻塞,线程将会失去CPU的使用权。虽然当前的网络I/O有一些解决办法,如一个客户端一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其他线程工作,还有为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本,但还是有一些使用场景下仍然是无法解决的。如当前一些需要大量HTTP长连接的情况,像淘宝现在使用的Web旺旺,服务端需要同时保持几百万的HTTP连接,但并不是每时每刻这些连接都在传输数据,这种情况下不可能同时创建这么多线程来保持连接。
磁盘I/O调优
提升I/O性能的方法有:
- 增加缓存,减少磁盘访问次数
- 优化磁盘的管理系统,设计最优的磁盘方式策略
- 设计合理的磁盘存储数据块,以及访问这些数据块的策略
网络I/O优化
-
减少网络交互的次数。要减少网络交互的次数通常需要在网络交互的两端设置缓存
-
减少网络传输数据量的大小。减少网络数据量的办法通常是将数据压缩后再传输,如在HTTP请求中,通常Web服务器将请求的Web页面gzip压缩后再传输给浏览器。
-
尽量减少编码。通常在网络I/O中数据传输都是以字节形式进行的,也就是说通常要序列化。所以在经过网络I/O传输时,尽量直接以字节形式发送,也就是尽量提前将字符转化为字节,或者减少字符到字节的转化过程。
同步与异步
所谓同步就是一个任务的完成需要依赖另外一个任务是,只有等待被依赖的任务完成后,依赖的任务才能完成。这是一种可靠的任务序列。要成功都成功,要失败都失败,两个任务的状态可以保持一致。而异步不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好地比喻同步与异步操作。
同步非阻塞:提升I/O性能的常用手段,就是将I/O的阻塞改成非阻塞方式,尤其在网络I/O是长连接同时传输数据也不是很多的情况下,提升性能非常有效。
异步阻塞:这种方式在分布式数据库经常用到,例如,在一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份备份记录写到其他机器上,这些备份记录通常都采用异步阻塞的方式写I/O。
设计模式解析之适配器模式
适配器模式的功能很好理解,就是把一个类的接口变换成客户端所能接受的另一个接口,从而使两个接口不匹配而无法在一起工作的两个类能够在一起工作。
通常被用在一个项目需要引用一些开源框架来一起工作的情况下,这些框架的内部都有一些关于环境的信息的接口,需要从外部传入,但外部的接口不一定能匹配。
Java I/O中的适配器模式
适配器的作用就是将一个接口适配到另一个接口,在Java的I/O类库有很多这样的需求,如将字符串数据转变成字节数据保存到文件中,将字节数据转变成流数据等。
装饰器与适配器模式都有一个别名就是包装模式,它们的作用看似都是起到包装一个类或对象的作用,但是使用它们的目的是很不一样的。适配器模式的意义是要将一个接口转变成另一个接口,它的目的是通过改变接口来达到重复使用的目的;而装饰器模式不是要改变被装饰对象的接口,而恰恰要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方法而提升性能。