使用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
除了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)
文件复制操作:
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最为重要的操作。
- 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);
}
}
虽然使用ByteBuffer读文件比Stream方式快了许多,但这不足以表明Stream方式与ByteBuffer方式有如此之大的差距。由于ByteBuffer是将文件一次性读入内存再做后续处理,而Stream方式则是边读文件边做处理数据。虽然Stream也采用了BufferInputStream缓冲区组件,但是也NIO的优势。