18、NIO

1、简介

  • 现在使用NIO的场景越来越多,很多网上的技术框架或多或少的使用NIO技术,譬如Tomcat,Jetty。学习和掌握NIO技术已经不是一个JAVA攻城狮的加分技能,而是一个必备技能。
    NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作

NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

  • 基于缓存区

    • 它一个buffer
  • 基于通道

    • 它有一个channel

    NIO与IO的区别

    • 区别一
        - IO流是基于Stream(流)
        - NIO它是面向缓冲区及基于通道的IO操作
    • 区别二
        - IO流是基于阻塞
        - NIO它是非阻塞
       NIO将以更加高效的方式对文件进行读写操作

缓冲区(Buffer)

什么是缓冲区

  • 一个用于特定基本数据类型的容器。与通道Channel打交道

  • 作用: Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。

  • 通道(Channel)就像铁轨,缓冲区(Buffer)就像矿车,
    在这里插入图片描述

Buffer的基本属性

  • 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。

  • 限制 (limit) :第一个不应该读取或不应该写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。

  • 位置 (position): :下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
    在这里插入图片描述

缓冲区的用法

NIO类下有个类叫Buffer,Buffer类下又有ByteBuffer(字节缓冲区)等

package bytebuffer用法;

import java.nio.ByteBuffer;
import org.junit.Test;
public class MainTest {

	//单元测试用法:ByteBuffer用法
	@Test
	public void testBuffer()
	{
		//创建分配容量长度为10的字节缓冲区,缓冲区名为buf
		ByteBuffer buf=ByteBuffer.allocate(10);
//allocate()是静态方法,不需要依赖类的创建,直接类名点静态方法名
		System.out.println("position="+buf.position()); //0,刚开始操作缓冲区的位置在0下标
		System.out.println("limit="+buf.limit()); //10
		System.out.println("capacity="+buf.capacity()); //10
		
		String putMessage="abcd";
		//将上面的字符串转换成字节数组cbuf
		byte []cbuf=putMessage.getBytes();
		//上面创建的字节缓冲区buf调用put方法,将字节数组cbuf的元素放入缓冲区buf中
		//从字节数组cbuf的下标为0的元素开始到数组的长度
		buf.put(cbuf, 0, cbuf.length);
		
		//你已经把abcd放到缓冲区中了,abcd对因美国下标0123
		//此时缓冲区操作指向的位置是4
		System.out.println("position="+buf.position()); //4
		System.out.println("limit="+buf.limit()); //10
		System.out.println("capacity="+buf.capacity()); //10
		
		//从缓冲区读取数据
		
		//方式1:调用get方法单个或者多个get
		//将position值设置为0,使从第一个元素取出(你上面的position已经指向4了,
		//但是字节缓冲区下标为4的位置是空的,上面我给缓冲区的容量是10)
	/*	buf.position(0);
		System.out.println((char)buf.get());//a
		System.out.println((char)buf.get());//b
		System.out.println((char)buf.get());//c
		System.out.println("position="+buf.position()); //现在position为3(有个元素d)
		System.out.println("limit="+buf.limit()); //10
		System.out.println("capacity="+buf.capacity()); //10
*/		
		//方式2:翻转成读模式
		buf.flip();
/*	flip()方法的源码	
	public final Buffer flip(){
		limit = position;
		position = 0;
		mark = -1;
		return this;//这句可不看,别纠结
	}	*/
		
		System.out.println("position="+buf.position()); //4
		System.out.println("limit="+buf.limit()); //10
		System.out.println("capacity="+buf.capacity()); //10
		
		byte cbuf2[]=new byte[buf.limit()];
		//从缓存区读取数据
		System.out.println("从缓存区读取数据");
		buf.get(cbuf2, 0, cbuf2.length);
		System.out.println(new String(cbuf2,0,cbuf2.length));
	}
}

上面的例子单单是Buffer的操作,只是演示如何使用,其实一般Buffer都与channel结合使用才会高效

通道(Channel)

Channel就是表示 IO 源与目标打开的连接,通过Channel可以将数据讲到缓冲区,也就是说Channel直接能够与缓冲区进行读、写操作。

2、Channel有哪些?

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • 以下都是针对网络通讯的通道
    • DatagramChannel:通过 UDP 读写网络中的数据通道。
    • SocketChannel:通过 TCP 读写网络中的数据。
    • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
      从这里可以看出,NIO其实更多的用于网络编程。
      这里只讲FileChannel。其他会在网络编程再讲。

FileChannel的用法

针对目标文件打开的连接,建立一个通道

用法一:读取文件到内存

package FileChannel用法;

import org.junit.Test;

public class MainTest {

	@Test
	public void testFileChannel01() throws IOException
	{
		//创建FileInputStream对象
		FileInputStream fileInputStream=new FileInputStream(new File("a.txt"));
		//获取FileChannel对象
		FileChannel fc=fileInputStream.getChannel();
		
		//定义一个ByteBuffer
		ByteBuffer buf=ByteBuffer.allocate(12);
		//position=0,limit=12,cap=12
		long len=-1;
		
		while((len=fc.read(buf))!=-1)
		{
			//设置ButeBuffer为读取模式
			buf.flip();
			String message=new String(buf.array(),0,buf.limit());
			System.out.print(message);
			
			/*byte []cbuf=new byte[buf.limit()];
			buf.get(cbuf, 0, cbuf.length);
			String message=new String(cbuf,0,cbuf.length);
			System.out.print(message);*/
			
			buf.clear();
		}
	}
}

用法二:复制文件功能

  • 通过IO流复制文件内容
    在这里插入图片描述
    文件1复制到文件2
@Test
	public void testCopyFile01() throws IOException
	{
		long start = System.currentTimeMillis();
		
		FileInputStream srcFileInputStream=new FileInputStream(new File("D:\\1.mp4"));//源文件
		FileOutputStream destFileOutputStream=new FileOutputStream(new File("D:\\2.mp4"));//复制到的目标文件
		//获取针对1.mp4文件的FileChannel对象
		FileChannel fc=srcFileInputStream.getChannel();
		//获取针对2.mp4文件的FileChannel对象(看上图)
		FileChannel destFc=destFileOutputStream.getChannel();
		//创建ByteBuffer对象
		//使用非直接方式创建ByteBuffer
		ByteBuffer buf = ByteBuffer.allocate(1024);
		//给缓冲区buf的空间为1k。上面说过缓冲区看以看作矿车,缓冲区buf文件1拿了1k的数据
		//搬到文件2,搬完后又再从文件1继续般1k的数据到文件2
		
		while(fc.read(buf)!=-1)
		{
			//配置ByteBuffer的可读模式
			buf.flip();
			destFc.write(buf);//边读边写到文件
			//清除缓存区
			//position=0;
			//limit=cap
			buf.clear();
		}
		
		if(fc!=null)
		{
			fc.close();
		}
		
		if(destFc!=null)
		{
			destFc.close();
		}
		
		if(srcFileInputStream!=null)
		{
			srcFileInputStream.close();
		}
		
		if(destFileOutputStream!=null)
		{
			destFileOutputStream.close();
		}
		
		long end = System.currentTimeMillis();
		System.out.println("耗费时间为:" + (end - start));
		
	}
  • 通过channel 的映射内存实现文件复制功能
@Test
	public void testCopyFile02() throws IOException
	{
		long start = System.currentTimeMillis();
		
		//建立1.mp4的通道(就是连接点)
		FileChannel fileChannelA=FileChannel.open(Paths.get("d:/1.mp4"), StandardOpenOption.READ);
		FileChannel fileChannelB=FileChannel.open(Paths.get("d:/2.mp4"), 
				StandardOpenOption.WRITE, 
				StandardOpenOption.READ, 
				StandardOpenOption.CREATE);
		
		//建立fileChannelA通道的映射内存
		MappedByteBuffer inMappedBuf = fileChannelA.map(MapMode.READ_ONLY, 0, fileChannelA.size());
		//建立fileChannelB通道的映射内存
		MappedByteBuffer outMappedBuf = fileChannelB.map(MapMode.READ_WRITE, 0, fileChannelA.size());
		
		//创建一个字节数组
		byte[] dst = new byte[inMappedBuf.limit()];
		inMappedBuf.get(dst);
		outMappedBuf.put(dst);
		
		//关闭通道
		fileChannelA.close();
		fileChannelB.close();
		
		
		long end = System.currentTimeMillis();
		System.out.println("耗费时间为:" + (end - start));
	}
  • 支持channel之间的数据复制功能
@Test
	public void testCopyFile03() throws IOException
	{
		long start = System.currentTimeMillis();
		
		FileChannel fileChannelA=FileChannel.open(Paths.get("d:/1.mp4"), StandardOpenOption.READ);
		FileChannel fileChannelB=FileChannel.open(Paths.get("d:/2.mp4"), 
				StandardOpenOption.WRITE, 
				StandardOpenOption.READ, 
				StandardOpenOption.CREATE);
		
		fileChannelB.transferFrom(fileChannelA, 0, fileChannelA.size());
		
		//关闭通道
		fileChannelA.close();
		fileChannelB.close();
		
		long end = System.currentTimeMillis();
		System.out.println("耗费时间为:" + (end - start));
		
	}
  • 通过以上三个方式的性能对比,用法二的最后一种不仅代码少效率还高,中间次之,第一种最慢。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值