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操作来完成,提升了读写操作时的性能,不过也带来了额外的创建和销毁时的代价,直接缓冲区一般是常驻内存,会增加内存开销,一般用在对性能较高的情况。以下通过三个简单的例子来理解流和通道对文件复制的操作。
        //使用流来实现文件的复制
	 public static void copyFileByStream(String src,String dest) throws IOException{
		 FileInputStream in=new FileInputStream(src);
	        File file=new File(dest);
	        if(!file.exists())
	            file.createNewFile();
	        FileOutputStream out=new FileOutputStream(file);
	        int c;
	        byte buffer[]=new byte[1024];  //每次读取的字节数
	        while((c=in.read(buffer))!=-1){
	                out.write(buffer);        
	        }
	        in.close();
	        out.close();
	    }
	 //使用ByteBuffer作为缓冲区来实现文件的复制
	 public static void copyFileByByteBuffer(String src,String dest)  throws IOException{
	        ByteBuffer buffer = ByteBuffer.allocateDirect(32*1024);//分配ByteBuffer的容量大小
	        FileInputStream in=new FileInputStream(src);
	        FileOutputStream out=new FileOutputStream(dest);
			FileChannel s = in.getChannel();
			FileChannel d = out.getChannel();  
			while(s.read(buffer)>0||buffer.position()!=0){ //通过在通道上使用ByteBuffer来传输,不需要记录每次实际读取的字节数
					buffer.flip();
					d.write(buffer);
					buffer.compact();
				}
		
	    }
	 
	 //使用通道的传输方法来实现文件的复制
	 public static void copyFileByChannelTransfer(String src,String dest)  throws IOException{
	        ByteBuffer buffer = ByteBuffer.allocateDirect(32*1024);
	        FileInputStream in=new FileInputStream(src);
	        FileOutputStream out=new FileOutputStream(dest);
			FileChannel s = in.getChannel();
			FileChannel d = out.getChannel();
			s.transferTo(0, s.size(), d);//直接从一个通道传输到另一个通道。
			
	    }
由以上可以看出,使用ByteBuffer类不需要像流一样记录每次实际读取的字节数,只要分配一个固定大小的ByteBuffer缓冲区就行了,文件通道的transferTo方法使得数据传输更加简单。
对大文件的操作一般使用ByteBuffer的子类MappedByteBuffer,该类将系统的内存地址映射到要操作的文件上,操作这些内存地址就相当于读取文件的内容,这样就大大提高了操作文件的性能。具体用法看相关文档。
FileChannel类还有lock和tryLock方法可对文件进行加锁,但是该加锁是在虚拟机级别的,对于虚拟机上的多线程程序,不能用这种加锁机制来协同不同的线程对文件的访问。FileChannel类的加锁是应用程序与应用程序之间的加锁。
由于传统的Socket类和ServerSocket类中提供的与建立连接和数据传输相关的方法都是阻塞式的,套接字通道提供了非阻塞式的和多路复用的套接字连接。多路复用套接字需要通过一个选择器(Selector)来对所有的套接字通道进行管理监听,当其中的某些套接字通道上有Selector感兴趣的事件发生的时候,这个通道就会变成可用的状态,然后可以进行各种通道操作。不过需要在一开始的时候把我们需要监听的套接字通道注册到选择器(Selector)上,以表明我们需要由选择器来监听这个套接字通道。
网上有一比较好的隐喻可以说明套接字的阻塞和非阻塞方法,摘抄如下:

一辆从A开往B的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?

1.司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。(类似阻塞式)

2.每个人(相当于套接字通道)告诉售票员(相当于Selector)自己的目的地(相当于套接字的事件),然后睡觉,司机(相当于CPU)只和售票员交互,到了某个点由售票员通知乘客下车。(类似非阻塞),很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是CPU。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客(线程)都在睡觉(休眠),只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。




参考:《深入理解Java7》成富

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值