设计目的
- NIO的设计目的是为了让Java程序员可以少些很多代码而实现高速的IO。NIO自动实现填充和提取缓冲区的转移工作。我们只要写一个循环将数据写入一个指定容量的缓冲区,需要写入到外部文件则一次性写入即可。
- 缓冲区通常是一个字节数组,但也可以指定任何类型的数组。
- 在不加字节数组循环读取文件的时候,IO实际是一个字节一个字节地读取的,输出也是一个字节一个字节地输出,这很耗费时间。而一个数组一个数组地读取无疑要快得多。
数据流动过程
- 任何来源地的数据都要先通过一个channel对象,然后读取到Buffer(缓冲区)中;
- 去到任何目的地的数据都要先读取到缓冲区中,然后写入通道。
- 也就是说,你永远不会讲字节直接写入通道中,而是将它们写入一个缓冲区。同样,你不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这些字节。
- 若果一个Channel类实现了ReadableByteChannel接口,说明它是可读的,WritableByteChannel则可写,都实现了则可读可写。
- 两种获取channel对象的方法
- 读文件:
- FileInputStream关联文件
- 从FileInputStream获取channel对象
- 创建Buffer缓冲区
- 从channel循环读取数据到缓冲区
关闭通道
FileInputStream fin=new FileInputStream("d://test4.txt"); //从FileInputStream获取通道 FileChannel fc=fin.getChannel(); //创建缓冲区 ByteBuffer buffer=ByteBuffer.allocate(1024); //allocate:分配 //将数据从通道读到缓冲区中 int len=0; while ((len = fc.read(buffer))!=-1){ //初始化游标 buffer.flip(); byte[] bytes=buffer.array(); String info=new String(bytes); System.out.println(info); //sout:每执行一次会把光标置于下一行首位,下一次执行会在这个位置打印,否则就会在文本后面多出一个空行 //清空数组 buffer.clear(); } fc.close();
- 写文件
- 关联目标文件
- 获取通道
- 创建Buffer
- 循环读取数据到缓冲区
- 写入通道
关闭通道
FileOutputStream fout=new FileOutputStream("d://test4b.txt"); //从FileOutputStream中获取一个通道 FileChannel foutc=fout.getChannel(); //创建一个缓冲区 ByteBuffer buffer1=ByteBuffer.allocate(1024); //单位是B,1024B=1KB //数据源 String message="Pigpig is my sister!"; //将数据读取到缓冲区 buffer1.put(message.getBytes()); //缓冲区自动为message分配空间 //或者这样准备数据源 /* ByteBuffer buffer3=Charset.forName("utf-8").encode("你好 你好 你好 你好 你好!"); */ //初始化游标 buffer1.flip(); //从缓冲区写入通道 foutc.write(buffer1); foutc.close();
- flip():把position游标(指向下一个要读取获取存入的位置)置于数组首位,读取的时候会从游标位置开始读取,否则游标还在末位,所以必须刷出
拷贝文件:循环读取数据源到缓冲区,一次性写入目标文件
FileInputStream fin2=new FileInputStream("d://test4.txt"); FileOutputStream fout2=new FileOutputStream("d://test4c.txt"); FileChannel finc2=fin2.getChannel(); FileChannel foutc2=fout2.getChannel(); ByteBuffer buffer2=ByteBuffer.allocate(1024); int len=0; while ((len = finc2.read(buffer2))!=-1){ //初始化游标 buffer2.flip(); foutc2.write(buffer2); //清空数组 buffer2.clear(); } fout2.close(); finc2.close();
- clear():将游标置为0,limit(还能存多少)置为容量,但数组内容并没有真正清空;否则上个循环的数据还在里面,导致数组不可用;不过上面还是需要flip(),因为read之后游标会跑到最后去
- ByteBuffer:最常用的缓冲区类型;不只是一个数组,还提供了对数据的结构化访问,可以跟踪系统的读写进程。
- Java提供的缓冲区类型,它们实现了Buffer接口:
阻塞:
Scanner sc=new Scanner(System.in); int i=sc.nextInt();
- 在返回之前,当前线程会被挂起,一直处于等待状态
- 阻塞IO:一旦输入、输出工作没有完成,则程序阻塞,知道输入、输出完成为止。
- 非阻塞:非阻塞IO并非完全非阻塞,只是设置了超时来读取数据,未超时之前程序阻塞,超时之后,程序结束
- 传统的IO都是阻塞的,当一个线程被read()或write()时,该线程被阻塞,知道有数据被请求到,否则该线程不能执行其他任务。所以,万一请求资源被占用,整个程序就相当于卡死了。比如当缓冲区里没有数据的时候,线程会一直等待。属于同步阻塞IO。
- NIO是非阻塞模式的,请求不到资源的时候线程可以执行其他任务。但是线程需要是不是去询问是否能请求到资源,从而引起不必要的CPU资源浪费当缓冲区没有数据的时候,线程一直返回0,直到有了再返回读到的数据。NIO属于同步非阻塞IO。
- 不过启用非阻塞模式是需要额外的代码的,好像不是默认的。
- 异步阻塞IO:程序发起一个IO操作后,就去做其他事情了,不等待IO操作完成,等内核完成IO操作后会通知程序。那为什么说是阻塞的呢?因为它是通过select系统调用来实现的,而select函数本身的实现方式是阻塞的。而采用select函数的好处就是它可以同时监听多个文件句柄,从而提高系统的并发性。
- 异步非阻塞:好像和异步阻塞也差不多,看不出有什么区别
Selector
实现监听的效果,通过一个线程管理多个channel,从而实现管理多个网络连接的目的。它是NIO核心组件中的一个,用于检查一个或者多个channel的状态是否处于非阻塞(可读|可写),我们可以将channel注册到selector中,以实现selector对其管理的目的。
NIO写大文件
直接用channel+ByteBuffer分块写入
/** * @Author haien * @Description nio分块写大文件,耗时近30秒 * @Date 2018/11/24 **/ public class WriteBigFile { //待写入内容总大小 private static final long LEN=2L*1024*1024*1024; //2G //每次写多少进去 private static final int DATA_CHUNK=128*1024*1024; //128M;chunk:块 public static void writeWithFileChannel() throws IOException { File file=new File("e:/fc.dat"); if(file.exists()){ file.delete(); } //用任意访问的方式打开文件,指定要来读写它 RandomAccessFile raf=new RandomAccessFile(file,"rw"); FileChannel fileChannel=raf.getChannel(); //存储待写入内容 byte[] data=null; //缓冲数组 ByteBuffer buf=ByteBuffer.allocate(DATA_CHUNK); //单位转换,128*1024*1024B->128M int dataChunk=DATA_CHUNK/1024/1024; //未写入的内容大小 long len; for(len=LEN;len>DATA_CHUNK;len-=DATA_CHUNK){ System.out.println("Write a data chunk: "+dataChunk+"MB"); buf.clear(); data=new byte[DATA_CHUNK]; //省略数据准备 for(int i=0;i<DATA_CHUNK;i++){ //把数据放到缓冲区里来 buf.put(data[i]); } //清空数据存储区 data=null; buf.flip(); //从缓冲区写入通道 fileChannel.write(buf); //把通道里的内容强制刷出 fileChannel.force(true); } //最后一次可能不足DATA_CHUNK if(len>0){ System.out.println("Write rest data chunk: "+len/1024/1024+"MB"); //剩下的比较少,可以从本地内存中分配 buf=ByteBuffer.allocateDirect((int)len); data=new byte[(int)len]; for(int i=0;i<len;i++){ buf.put(data[i]); } buf.flip(); fileChannel.write(buf); fileChannel.force(true); data=null; } fileChannel.close(); raf.close(); } }
- channel.force(true):将通道里的数据强制刷出
- ByteBuffer.allocateDirect(): 在本地内存中分配缓冲区
- ByteBuffer.allocate():在jvm堆中分配
- 本地缓冲区又叫直接缓冲区,相对堆缓冲区性能较高,但是本地的内存不能被jvm垃圾回收机制回收,而是自动调用JNI方法回收。
- 由于垃圾回收成本较高,堆内存未耗尽时,jvm不会回收垃圾。如果为堆分配过大的内存(也即是使用allocateDirect),本地内存就会相应减少。堆缓冲区的性能已经相当高,只有确实需要再提高性能时才考虑使用本地缓冲区。
- 本例即是最后只剩一点没写才使用本地缓冲区。
channel.map()+MappedByteBuffer
- 一般操作系统的内存分为两部分:物理内存和虚拟内存。虚拟内存一把指使用的是页面映像文件,即硬盘中的某些特殊的文件。操作系统负责页面文件内容的读写,这个过程叫页面中断/借还。
- MapppedByteBuffer也是类似的,,它直接将文件映射到内存(虚拟内存)。通常可以映射整个文件,如果文件比较大的话可以进行分段映射。
- channel.map(int mod,long start,long size): 把文件从start开始的size大小的部分映射为内存映射文件,返回MappedByteBuffer,mode指定该内存映像文件的访问方式:
- READ_ONLY
- READ_WRITE: 对返回的缓冲区的更改将传播到文件
- PRIVATE:对返回的缓冲区的更改不会传播到文件
- 相比ByteBuffer,它读写更快,而且可以随时随地写入。
- 内含的缓冲区是直接缓冲区,所以被它打开的文件只有在垃圾收集时才会被关闭,而这个点是不确定的,在此之前MapppedByteBuffer的对象一直持有文件的句柄。
channel把MapppedByteBuffer map出来但却不提供unmap方法,所以我们自己实现一个来释放文件的句柄。
//48s public static void wirteWithMappedByteBuffer() throws IOException { File file=new File("e:/mb.dat"); if(file.exists()){ file.delete(); }else{ file.createNewFile(); } RandomAccessFile raf=new RandomAccessFile(file,"rw"); FileChannel fileChannel=raf.getChannel(); byte[] data=null; int start; MappedByteBuffer mbb=null; long len; int dataChunk=DATA_CHUNK/1024/1024; for(start=0,len=LEN;len>=DATA_CHUNK;len-=DATA_CHUNK,start+=DATA_CHUNK){ System.out.println("Write a data chunk: "+dataChunk+"MB"); mbb=fileChannel.map(FileChannel.MapMode.READ_WRITE,start,DATA_CHUNK); data=new byte[DATA_CHUNK]; mbb.put(data); data=null; } if(len>0){ mbb=fileChannel.map(FileChannel.MapMode.READ_WRITE,start,DATA_CHUNK); data=new byte[DATA_CHUNK]; mbb.put(data); data=null; } //release MappedByteBuffer unmap(mbb); fileChannel.close(); } /** * @Author haien * @Description 一般用了MappedByteBuffer之后都是这样释放资源的,淡单单channel.close()是不够的 * @Date 2018/11/24 * @Param [mappedByteBuffer] * @return void **/ public static void unmap(final MappedByteBuffer mappedByteBuffer){ try { if (mappedByteBuffer == null) { return; } //把残留的内容强制刷出 mappedByteBuffer.force(); AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override @SuppressWarnings("restriction") public Object run() { try { Method getCleanerMethod = mappedByteBuffer.getClass() .getMethod("cleaner", new Class[0]); getCleanerMethod.setAccessible(true); sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod .invoke(mappedByteBuffer, new Object[0]); cleaner.clean(); } catch (Exception e) { e.printStackTrace(); } return null; } }); }catch (Exception e){ e.printStackTrace(); } }
ByteArrayINputstream+channel
/** * @Author haien * @Description 中间转换成字节数组再写入文件 19s * @Date 2018/11/25 * @Param [] * @return void **/ public static void writeWithByteArray() throws IOException { File file=new File("e:/ba.dat"); if(file.exists()){ file.delete(); }else{ file.createNewFile(); } RandomAccessFile raf=new RandomAccessFile(file,"rw"); FileChannel fileChannel=raf.getChannel(); //数据准备 byte[] data=null; //把数据包装为字节数组 ByteArrayInputStream bis=null; //把字节数组转化为流 ReadableByteChannel byteChannel=null; long len=0; long start=0; //单位转换 int dataChunk=DATA_CHUNK/1024/1024; for(len=LEN;len>=DATA_CHUNK;len-=DATA_CHUNK,start+=DATA_CHUNK){ System.out.println("Write a data chunk: "+dataChunk+"MB"); //数据准备 data=new byte[DATA_CHUNK]; //包装成字节数组输入流 bis=new ByteArrayInputStream(data); //获取通道 byteChannel =Channels.newChannel(bis); //写到目标文件的channel中 fileChannel.transferFrom(byteChannel,start,DATA_CHUNK); //transferFrom:将其他通道从start字节开始写指定字节数到文件通道中 data=null; } if(len>0){ System.out.println("Write rest data chunk: "+len/1024/1024+"MB"); data=new byte[(int)len]; bis=new ByteArrayInputStream(data); byteChannel=Channels.newChannel(bis); fileChannel.transferFrom(fileChannel,start,len); data=null; } fileChannel.close(); byteChannel.close(); }
参考文章