操作系统IO五类模型
IO操作的两个阶段:1.等待数据的到来,当数据到来时将数据拷贝到内核临时缓冲区;2. 将数据从内核临时缓冲区拷贝到用户缓冲区;
阻塞IO、非阻塞IO:阻塞IO就是进程一直等待数据的到来直到抵达超时时间;非阻塞IO就是采用“轮询”的方式询问操作是否准备就绪,是一种浪费CPU的方式;在阻塞IO上上进一步改进,一个阻塞IO监控一个套接字比较改进为使用IO复用(select 、poll),一次监控多个套接字,减少轮询次数,轮询模式一定程度上牺牲了实时性;当然select一次管理的socket的数量有上限,比如在linux 2.6.15内核中最大值为1024。
当然IO复用一定程度上解决了我们一些问题,但是他的副作用也很明显,对CPU的压力、牺牲的实时性。接下来还有2种方式时异步IO模式,型号驱动IO模式和异步IO模式;异步IO与异步信号IO的区别是异步IO是内核所有操作处理完毕后才会通知,而信号异步IO是内核执行过程中也会通知,比如数据准备完毕后的通知。异步IO是最理想的工作方式,但是目前在实践中一直没有稳定的内核版本推出。
在异步IO未成熟的情况下,解决select模式的socket句柄数限制,在Linux 2.6 内核中引入了epoll IO多路复用技术。poll每次调用时对集合进行线性扫描,而epoll只会对活跃的socket进行操作,这样IO效率不随FS数量的增加而线性下降;其次,为了解决数据从内核态到用户态的拷贝,在用户空间和内核空间的划出一片共同区域mmap实现数据交换;基于事件驱动,避免每次都把所有fd都扫描一遍
BIO(Blcok IO)
基本类熟悉:
- File类
- InputStream OutputStream 面向字节形式的I/O
- Reader Writer 兼容Unicode与面向字符的I/O; 提供了内置锁lock,对临界区考虑了线程安全性,默认使用自身对象作为对象锁,否则使用构造函数传入的锁对象。
- FilterInputStream FilterOutputStream 对InputStream outStream进行包装,实现装饰器模式。为什么要使用装饰器模式?
- FilterReader FilterWriter
- InputStreamReader OutputStreamWriter 适配器模式,提供了对InputStream、OutputStream的兼容
- bufferedInputStram bufferedOutputStream 通过 属性添加volatile/方法sychronized关键字实现线程安全; bufferedReader bufferedWriter 通过synchronized+内部对象锁实现临界区的并发访问;通过自己写测试类,确认方法synchronized是对象锁this,多一个synchronized方法之间互斥访问;因为后者在已经方法就进行加锁,所以两种方式在执行性能上相差无几,只是两种代码编写风格,synchronized写在方法上会更优雅一些,因为可以让使用者明确知晓方法为线程安全的,此次对象锁也会增加代码工作量。如果一个方法中只有很少一部分访问临界区,那么采用编写对象lock锁性能更高。
理解:
两个对称性:1. InputStream 与 OutputStream 对称,分别位于流管道的两端;2. (inputStream、outputStream)字节操作与字符(reader、writer)操作的对称性;
两个设计模式:装饰器模式 和 适配器模式;IO库使用装饰器(组合)模式,不采用继承的原因是,如果采用继承,那么在多个实现类上每一个都需要添加子类,会导致类爆炸,采用装饰器模式,让原始类与辅助功能类进行拆分,可以做到辅助功能类对其它原始类复用。其次可以做到原始实现类对修改关闭,装饰器对扩展开放。这个就像我们我们提供豆制品(Component)的一种实现原味的豆腐脑(ConcreteComponent)、和原味豆浆(ConcreteComponent),并且提供了佐料 糖、盐、酱油、花生米等(Decorator),每一种作料都可以用来装饰豆腐脑或者豆浆形成新的口味。佐料需要接受一个豆制品的具体实现素材才能制作新口味饮品,所以必须把豆制品的引用传递给佐料。
为什么Decorator也需要实现Component接口呢?举个例子,豆腐脑加了酱油形成了酱油豆腐脑,如果酱油豆腐脑不是一种Component,那么用户要吃带花生米的酱油豆腐脑怎么办呢,没法扩展了,因为花生米只接受豆制品(Component),所以佐料也实现Component接口这样通过Decorator组建的新产品可以作为另外一个佐料的输入源进一步被装饰。这样不仅支持了纵向扩展,也支持了水平扩展。
为什么Decorator需要一个抽象父类呢?因为没必要为每一个装饰后的类都重新写一个装饰器,其次抽象装饰器父类可以做到ConcreteComponent与Decorator解耦。
NIO(Non-blcok IO)
NIO里面重要的引入了三个概念,通道Channels、缓冲区Buffers、选择器Selectors。在BIO中IO基于字节或字符,但NIO基于通道、缓冲区进行操作。
Buffer:
底层是一个固定长度数组,并有几个int对象标识一些指针,capacity >= limit >= position >= mark,通过这些指针标识写入的数据位置和可读数据的区域。一帮对象采用HeapByteBuffer,但堆大小有限,对于大文件的操作需要机遇磁盘IO进行处理,这个时候就需要借助MappedByteBuffer进行处理。
文件加锁:
通过Channel的lock()或tryLock()方法获得文件锁,文件锁依赖于操作系统实现,属于系统调用,也因此当文件被加锁以后,不仅是当前进程中的其它线程无法再次获得锁,其他任何进程(无论本地还是远程)都无法再对文件进行操作(具体还要取决于操作系统对文件是否支持共享锁,比如我的Mac系统就不支持共享锁)。但是由于这种机制高度依赖系统,所以实际应用中应该并不常用。而且FileLock是JVM持有的,而非线程持有,一个JVM只能持有一次,所以当已经持有锁的情况下同一JVM里的另一线程再次请求锁,会抛出OverlappingFileLockException,而非阻塞,即使是使用tryLock()方法,也不会直接返回null,同样抛出OverlappingFileLockException。
图形化demo学习工具:http://www.javanio.info/filearea/demos/