设计目的

  • NIO的设计目的是为了让Java程序员可以少些很多代码而实现高速的IO。NIO自动实现填充和提取缓冲区的转移工作。我们只要写一个循环将数据写入一个指定容量的缓冲区,需要写入到外部文件则一次性写入即可。
  • 缓冲区通常是一个字节数组,但也可以指定任何类型的数组。
  • 在不加字节数组循环读取文件的时候,IO实际是一个字节一个字节地读取的,输出也是一个字节一个字节地输出,这很耗费时间。而一个数组一个数组地读取无疑要快得多。

    数据流动过程

  • 任何来源地的数据都要先通过一个channel对象,然后读取到Buffer(缓冲区)中;
  • 去到任何目的地的数据都要先读取到缓冲区中,然后写入通道。
  • 也就是说,你永远不会讲字节直接写入通道中,而是将它们写入一个缓冲区。同样,你不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这些字节。
  • 若果一个Channel类实现了ReadableByteChannel接口,说明它是可读的,WritableByteChannel则可写,都实现了则可读可写。
  • 两种获取channel对象的方法
    • 使用Channels抽象类:ReadableByteChannel newChannel(InputStream in)
    • 通过经典的io类来获取

      实践

  • 读文件:
  1. FileInputStream关联文件
  2. 从FileInputStream获取channel对象
  3. 创建Buffer缓冲区
  4. 从channel循环读取数据到缓冲区
  5. 关闭通道

    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();
    
  • 写文件
  1. 关联目标文件
  2. 获取通道
  3. 创建Buffer
  4. 循环读取数据到缓冲区
  5. 写入通道
  6. 关闭通道

    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接口:
    • ByteBuffer
    • CharBuffer
    • ShortBuffer
    • IntBuffer
    • LongBuffer
    • FloatBuffer
    • DoubleBuffer

      阻塞和非阻塞

  • 阻塞:

    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();
    }
    

    参考文章