使用NIO提升性能

使用NIO提升性能

在软件系统中,由于I/O的速度要比内存慢,因此I/O读写在很多场合都会成为系统瓶颈,提升I/O速度对提升系统性能有着很大的好处。
在java的标准I/O中,提供了基于流的I/O实现,即InputStream和OutputStream。这种基于流的实现,以字节为单位处理数据,并且非常容易建立各种过滤器。
NIO就是New的I/O简称,它表示新一套的标准,具有以下特性:

  • 为所有的原始类型提供(Buffer)缓存支持
  • 使用Java.nio.charset.Charset作为字符编码解码解决方案。
  • 增加(Channel)对象,作为新的原始I/O抽象。
  • 支持锁和内存映射文件的文件访问接口
  • 提供了基于Selector的异步网络I/O

与流式I/O不同,NIO是基于块(Block)的,它以块的基本单位处理数据。在NIO中,最为重要的两个组件是缓冲Buffer和通道Channel。缓冲是一块连续的内存块,是NIO读写数据的中转池,通道表示缓冲数据的源头或者目的地,它用于向缓冲读取或写入数据,是访问缓冲的接口。

在NIO的实现中,Buffer是一个抽象类,JDK为每一个原生类型都创建了一个Buffer
原生类对应的Buffer
除了ByteBuffer外,其他每一种Buffer都具备完全一样的操作,唯一的区别仅在于他们所对应的数据类型。因为ByteBuffer多用于绝大多数标准IO操作接口,因此它有一些特殊的方法,有点类似Stream,但Stream是单向的。应用程序中不能直接对Channel进行读写操作,而必须通过Buffer来进行,比如在读一个Channel的时候,需要先将数据读入到相对应的Buffer,然后再Buffer中进行读取。

以一个简单的文件为例,在读取文件时,首先将文件打开,并取得文件的Channel:
public class Demo {
	
	public static void main(String[] args) throws Exception {
		String path="D:"+File.separator+"charTest.txt";
		FileInputStream fis=new FileInputStream(new File(path));
		FileChannel fc=fis.getChannel();
		//要从文件Channel中读取数据,必须使用Buffer
		ByteBuffer bf=ByteBuffer.allocate(1024);
		while(fc.read(bf)>0) {
		//把ByteBuffer从写模式,转变成读取模式,即使把position的位置变成0,
		//limit的位置变成position。因为这时候
        //已经把数据读到缓冲区了,所以要复位一下,才能读。(重置到开始的位置,开始读)
			bf.flip();
			  //下面这两个是把缓冲区的数据打印下来
			Charset charset=Charset.forName("UTF-8");
			System.out.println(charset.newDecoder().decode(bf).toString());
		}
		fc.close();
		fis.close();
	}
}
结果:
新中国成立了!!!
加油。
Buffer的基本原理

Buffer中有三个重要的参数:位置(position)、容量(capactiy)和上限(limit)
Buffer参数含义

文件复制操作:
public class Demo {
	
	public static void main(String[] args) throws Exception {
		String path1="D:"+File.separator+"charTest.txt";
		String path2="D:"+File.separator+"charTestNew.txt";
		FileInputStream fis=new FileInputStream(new File(path1));
		FileOutputStream fos=new FileOutputStream(new File(path2));
		
		FileChannel readFc=fis.getChannel();
		FileChannel writeFc=fos.getChannel();
		
		ByteBuffer bf=ByteBuffer.allocate(1024);
		while(true) {
			//这里是一次写入缓冲区
			bf.clear();
			int len=readFc.read(bf);
			if(len==-1) {
				break;
			}
			bf.flip();
			writeFc.write(bf);
		}
		readFc.close();
		writeFc.close();
	}
}
结果:成功将文件复制。
Buffer的相关操作:
Buffer是NIO最为核心的对象。
Buffer的创建可以通过两种方式,使用静态方法allocate()堆中分配缓冲区,或者从一个既有的数组创建
缓冲区。

1.从堆中分配缓冲区:
ByteBuffer bf=ByteBuffer.allocate(1024);

2.从已有数组创建
byte array[]=new byte[1024];
ByteBuffer buffer=ByteBuffer.wrap(array);

重置和清空缓冲区:
public final Buffer rewind();
public final Buffer clear();
public final Buffer flip();
3个函数有着类似的功能,它们都重置了Buffer对象,这里所谓的重置,只是重置了Buffer的各项标志位,
并不真正清空Buffer的内容。3个函数的功能上也有所不同。
函数rewind()将position置0,并清除标志位(mark)。它的作用在于为提取Buffer的有效数据做准备。

函数clear()也将position置0,同时将limit设置为capacity的大小,并清除了标志mark。由于清空
limit,因此便无法得知,Buffer内那些数据是真实有效的,这个方法用于为重新写Buffer做准备。

函数flip()先将limit设置到position所在的位置,然后将position置0,并清除标志位mark,它通常在
读写转换时使用。

Buffer重置函数作用

读/写缓冲区
对Buffer进行读写操作是Buffer最为重要的操作。
读写操作方法

  • get()方法:返回当前position上的数据,并将position位置向后移一位。
  • get(byte[] dst)方法:读取当前Buffer的数据到dst中,并恰当的移动position的位置。
  • get(int index)方法: 读取给定的index索引上的数据,不改变position位置。
  • put(byte b)方法:当前位置写入给定数据,position位置向后移一位。
  • put(int index,byte b)方法:将数据b写入当前的Buffer的index位置。
  • put(byte[] src)方法:将给定的数据写入当前的Buffer。

标志缓冲区
标志(mark)缓冲区是一项在数据处理时,很有用的功能。它就像书签一样,在数据处理过程中,可以随时处理当前位置。然后在任意时刻,回到这个位置,从而加快或简化数据处理流程。

  • public final Buffer mark();
  • public final Buffer reset();
    mark()用于记录当前的位置,reset函数用于恢复到mark所在的位置。
public class Demo {
	
	public static void main(String[] args) throws Exception {
		ByteBuffer byteBuffer=ByteBuffer.allocate(15);
		for(int i=0;i<10;i++) {
			byteBuffer.put((byte)i);
		}
		byteBuffer.flip();
		for(int j=0;j<byteBuffer.limit();j++) {
			System.out.print(byteBuffer.get()+" ");
			if(j==4) {
				byteBuffer.mark();//在第四个位置做mark
				System.out.print("mark at--"+j);
			}
		}
		System.out.println();
		byteBuffer.reset();
		while(byteBuffer.hasRemaining()) {
			System.out.print("remain"+byteBuffer.get()+" ");
		}
	}
}
结果:
0 1 2 3 4 mark at--45 6 7 8 9 
remain5 remain6 remain7 remain8 remain9 

复制缓冲区
复制缓冲区是指以原缓冲区为基础,生成一个完全一样的新缓冲区。方法如下:
public ByteBuffer duplicate();
这个函数处理复杂的Buffer数据很有好处,因为新生成的缓冲区和原缓冲区共享相同的内存数据,并且对任意一方的数据改动都是相互可见的,但两者又独立维护各自的position、limit和mark。

public class Demo {
	
	public static void main(String[] args) throws Exception {
		ByteBuffer bf=ByteBuffer.allocate(15);
		for(int i=0;i<10;i++) {
			bf.put((byte)i);
		}
		System.out.println("原缓冲区:"+bf);
		//复制缓冲区
		ByteBuffer bf2=bf.duplicate();
		System.out.println("新缓冲区:"+bf2);
		bf2.flip();//重置缓冲区。
		System.out.println("重置新缓冲区:"+bf2);
		bf2.put((byte)100);
		System.out.println("添加元素后的原冲区:"+bf.get(0));
		System.out.println("添加元素后的新缓冲区:"+bf2.get(0));
		}
}
结果:
原缓冲区:java.nio.HeapByteBuffer[pos=10 lim=15 cap=15]
新缓冲区:java.nio.HeapByteBuffer[pos=10 lim=15 cap=15]
重置新缓冲区:java.nio.HeapByteBuffer[pos=0 lim=10 cap=15]
添加元素后的原冲区:100
添加元素后的新缓冲区:100
可以看出:duplicate()方法产生了两个完全一样的Buffer缓冲区,并且他们各自维护自己的position,
对副本缓冲区进行put操作时,同样的位置原缓冲区也发生变化。

缓冲区分片
缓冲区分片使用slice()方法实现,它将在现有的缓冲区中创建新的子缓冲区,子缓冲区和父缓冲区共享数据,这个方法有助于将系统模块化,当需要处理Buffer的一个片段时,可以使用slice()方法取得一个子缓冲区,然后就想处理普通缓冲区一样,处理这个片段,无需考虑缓冲区的边界问题。

public class Demo {
	
	public static void main(String[] args) throws Exception {
		ByteBuffer bf=ByteBuffer.allocate(15);
		for(int i=0;i<10;i++) {
			bf.put((byte)i);
		}
		//取位置2~6的数据为子缓冲区
		bf.position(2);
		bf.limit(6);
		ByteBuffer bf2=bf.slice();
		System.out.println("子缓冲区:"+bf2);
		}
}
结果:
子缓冲区:java.nio.HeapByteBuffer[pos=0 lim=4 cap=4]

子缓冲区示意图
子缓冲区做了相应的修改意味着也修改了对应的父缓冲区中的数据。

public class Demo {
	
	public static void main(String[] args) throws Exception {
		ByteBuffer bf=ByteBuffer.allocate(15);
		for(int i=0;i<10;i++) {
			bf.put((byte)i);
		}
		//取位置2~6的数据为子缓冲区
		bf.position(2);
		bf.limit(6);
		ByteBuffer bf2=bf.slice();
		for(int j=0;j<bf2.capacity();j++) {
			byte num=bf2.get(j);
			num*=10;
			bf2.put(j,num);
		}
		bf.position(0);
		bf.limit(bf.capacity());
		while(bf.hasRemaining()) {
			System.out.print(bf.get()+" ");
			}
		}
}
结果:
0 1 20 30 40 50 6 7 8 9 0 0 0 0 0 

只读缓冲区
可以使用缓冲区对象的asReadOnlyBuffer()方法得到一个与当前缓冲区一致的,并且共享内存数据的只读缓冲区,只读缓冲区对于共享内存数据非常管用,当缓冲区作为参数传递给对象的某个方法时,由于无法确定该方法是否会破坏缓冲区数据,此时,使用只读缓冲区可以保证数据不被修改,同时因为只读缓冲区和原始缓冲区是共享内存块的,因此对原始缓冲区的修改,只读缓冲区也是可见的。

public class Demo {
	
	public static void main(String[] args) throws Exception {
		ByteBuffer bf=ByteBuffer.allocate(15);
		for(int i=0;i<10;i++) {
			bf.put((byte)i);
		}
		ByteBuffer bfRead=bf.asReadOnlyBuffer();
		bfRead.flip();
		while(bfRead.hasRemaining()) {
			System.out.print(bfRead.get()+" ");
		}
		System.out.println();
		bf.put(2,(byte)20);
		//一定要再进行重置,才可以继续读取,因为上面读完数据,position指针已经在最后了。
		bfRead.flip();
		while(bfRead.hasRemaining()) {
			System.out.print(bfRead.get()+" ");
		}
	}	
}
结果:
0 1 2 3 4 5 6 7 8 9 
0 1 20 3 4 5 6 7 8 9 
可以发现,修改原始缓冲区,相当于修改只读缓冲区的对应数据。只读缓冲区和原始缓冲区共享内存数据。
如果对只读缓冲区操作,会抛出ReadOnlyBufferException异常。

文件映射到内存
NIO提供了一种将文件映射到内存的方法,进行I/O操作,它可以比常规基于流的IO快很多,这个操作主要是由FileChannel.map()方法实现。

比如:
public class Demo {
	
	public static void main(String[] args) throws Exception {
		String path1="D:"+File.separator+"charTest.txt";
		FileInputStream fis=new FileInputStream(new File(path1));
		FileChannel fc=fis.getChannel();
		MappedByteBuffer map=fc.map(FileChannel.MapMode.READ_ONLY, 
		0, fis.available());
		while(map.hasRemaining()) {
			System.out.print((char)map.get());
		}
	}	
}

处理结构化数据–散射和聚集
散射就是将某个地方的数据读到一组Buffer中(不仅仅是一个Buffer)
聚集就是将一组Buffer中的数据写入到某个地方。
在散射读取中,通道依次填充每一个缓冲区,填满一个缓冲区后,它就开始填充下一个,在某种意义上,缓冲区数组就像一个大的缓冲区。
可以通过使用聚集写的方式创建文件,构建多个Buffer数组,每个数组有对应的格式内容。
JDK提供的各种通道中,DataGramChannel、FileChannel和SocketChannel都实现了这两个接口。

假设有有文件格式为“书名作者”,现通过聚集写操作创建该文件。
public class Demo {
	
	public static void main(String[] args) throws Exception {
		String path1="D:"+File.separator+"book.txt";
		
		ByteBuffer bBook=ByteBuffer.wrap("java编程思想".getBytes("utf-8"));
		ByteBuffer bAuthor=ByteBuffer.wrap("foregion".getBytes("utf-8"));
		ByteBuffer bb[]=new ByteBuffer[] {bBook,bAuthor};
		
		File file=new File(path1);
		if(!file.exists()) {
			file.createNewFile();
		}
		FileOutputStream fos=new FileOutputStream(file);
		FileChannel fc=fos.getChannel();
		fc.write(bb);
		fos.close();
	}	
}
成功将内容写到新创建的文件中。

使用散射读操作,将上面的书名和作者解析成两个字符串。
public class Demo {
	
	public static void main(String[] args) throws Exception {
		String path1="D:"+File.separator+"book.txt";
		
		//假设已知字符串大小以及长度
		ByteBuffer bBook=ByteBuffer.wrap("java编程思想".getBytes("utf-8"));
		ByteBuffer bAuthor=ByteBuffer.wrap("foregion".getBytes("utf-8"));
		int a=bBook.limit();
		int b=bAuthor.limit();
		
		ByteBuffer book=ByteBuffer.allocate(a);
		ByteBuffer author=ByteBuffer.allocate(b);
		ByteBuffer bbs[]=new ByteBuffer[] {book,author};
		
		
		File file=new File(path1);
		FileInputStream fis=new FileInputStream(file);
		FileChannel fc=fis.getChannel();
		fc.read(bbs);
		fis.close();
		String bookName=new String(bbs[0].array(),"utf-8");
		String authorName=new String(bbs[1].array(),"utf-8");
		System.out.print(bookName+"--"+authorName);
	}	
}

I/O方式速度比较

虽然使用ByteBuffer读文件比Stream方式快了许多,但这不足以表明Stream方式与ByteBuffer方式有如此之大的差距。由于ByteBuffer是将文件一次性读入内存再做后续处理,而Stream方式则是边读文件边做处理数据。虽然Stream也采用了BufferInputStream缓冲区组件,但是也NIO的优势。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值