Java IO与NIO的相关问题
流(Stream)是最早的Java对IO的抽象,而通道(Channel)是NIO对新Java对IO的抽象,通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。流和通道的基本单位都是字节,但是流是以字节数组作为缓冲区中介,而通道是以ByteBuffer来作为缓冲区中介。
流中包含的字节如流水一样,一旦流过去,就无法再使用。但由于流的实现是抽象类,在其子类中可以选择覆写父类的某些操作,所以子类输入输出流可能会有额外的控制操作,以便实现流的部分内容的重新读取,如流的标记(mark)和重置功能(reset),但并不是每一种流都有这两种功能,需要有相应的实现,可通过markSupported方法来判断是否支持标记功能。因为流无法复用,假如有一个流有多个接收者,那怎么办?上面介绍的标记和重置功能是其中一个方法,也可以把流中的全部数据保存到一个字节数组中,在不同的接受着中数据传递是通过这个字节数组来完成,而不是使用流的对象。其中流的标记和重置功能也是通过第二种方法实现的,只是这个字节数组的保存和操作是又流的某些子类来实现,如BufferedInputStream类。
调用流的read方法时,如果没有足够的数据可以用,则read方法会被阻塞,直到当前的流成功的完成准备为止。从流中读取的数据并不是马上进入目的介质,而是先放入字节数组等缓冲区,等合适的时机再执行实际的写入操作。当然可以通过调用flush方法强制写入,注意在缓冲区满或者流关闭的时候,也会自动执行实际的写入操作。
字节流是处理字节的,Java IO还有一种处理字符的字符流,即java.io.Reader类和java.io.Writer类及其子类,字符类一般是通过字节流InputStream类和OutputStream类来创建,对应的是InputStreamReader类和OutputStreamWriter类,只要指定编码格式就行了。
Java NIO的通道使用的基本缓冲区是ByteBuffer,其中有3个基本状态变量:容量(capacity),就是缓冲区的大小;读写限制(limit),总大小中允许读写的最大位置;读写位置(position),当前读写的位置。这三个状态变量都是以字节为单位的,假如是CharBuffer,则是以字符为单位,IntBuffer则是以整数为单位,等等。ByteBuffer的基本方法: clear,把读写限制设为缓冲区的容量,同时把读写位置设为0,flip方法,把读写限制设为当前的读写位置,同时把读写位置设为0,rewind方法,不改变读写限制,仅把读写位置设为0,compact方法,把当前读写位置到读写限制范围内的数据复制到内部存储空间的最前面,然后再把读写位置移动到紧接着复制完成的数据的下一个位置,读写限制设置为容量的大小。ByteBuffer的实现可分为直接缓冲区和非直接缓冲区,直接缓冲区直接使用操作系统底层的IO操作来完成,提升了读写操作时的性能,不过也带来了额外的创建和销毁时的代价,直接缓冲区一般是常驻内存,会增加内存开销,一般用在对性能较高的情况。以下通过三个简单的例子来理解流和通道对文件复制的操作。
一辆从 A 开往 B 的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?
1. 司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。 ( 类似阻塞式 )
2. 每个人(相当于套接字通道)告诉售票员(相当于Selector)自己的目的地(相当于套接字的事件),然后睡觉,司机(相当于CPU)只和售票员交互,到了某个点由售票员通知乘客下车。 ( 类似非阻塞 ),很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是 CPU 。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客 ( 线程 ) 都在睡觉 ( 休眠 ) ,只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。
参考:《深入理解Java7》成富 论坛:http://www.iteye.com/topic/834447