美文网首页
“IO与NIO ”重点概念整理

“IO与NIO ”重点概念整理

作者: 落雨松 | 来源:发表于2018-12-12 20:46 被阅读0次

    一、IO与NIO的区别

    1、传统IO
    面向流(输入输出流)、基于管道单向运输、是一个阻塞型IO。
    2、NIO
    面向缓冲区、基于通道双向传输、是非阻塞的。

    当我们在“文件、磁盘、网络” 与程序之间传输数据的时候,IO 通过 一个“管道 ”连接两者,然后通过建立“输入流”或者“输出流” 对数据进行输入和输出的操作,所以是单向的。
    而NIO通过连接一个“通道(Channel)” ,在此通道里建立一个“缓冲区(Buffer)”,把数据存放在这个缓冲区,又或者说,这个“缓冲区”相当于 数据传输两方的“媒人”,是双向关系的。

    二、NIO缓冲区的存取

    (一)概念:
    缓冲区在Java中负责数据的存取,底层由数组实现,用于存储不同数据类型的数据。根据数据类型的不同,提供了不同数据类型的缓冲区(除了boolean类型):
    ByteBuffer、IntBuffer、CharBuffer、ShortBuffer、、、等等。

    上述不同数据类型的缓冲区由 方法“allocate(指定大小)” 获取,如:

    //创建一个容量为1024字节的byte缓冲区
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    

    (二)存储数据的核心方法
    |----------get() ; //获取缓冲区中的数据
    |----------put() ;//向缓冲区中输入数据

    (三)缓冲区中的四个核心属性
    Buffer.java 源 码中 有四个属性:


    image.png

    |----------position : 位置, 表示正在操作的数据的位置。
    |----------limit : 界限,表示允许操作的缓冲区界限,limit后的位置不能|----------被读写。
    |----------capacity : 容量,表示数组的容量,也就是缓冲区的大小,一旦创建,不可以更改。
    |----------mark : 标记,用于标记position的位置。

    (:这些属性可以通过缓冲区对象(如上面的 “byteBuffer” ),获取实际值,如( byteBuffer.capacity();))

    三、NIO直接缓冲区与非直接缓冲区

    (一)基本概念
    |-----------非直接缓冲区:通过“allocate”方法分配的缓冲区,是建立在 JVM内存中的。
    如图:


    捕获.PNG

    (当用户程序需要读数据的时候,首先是从“物理磁盘”读数据到“内核地址空间”,然后“复制copy”一份到“用户地址空间”,用户程序再在从“用户地址空间中读取数据”,反之如图)

    |-----------直接缓冲区:通过“allocateDirect”方法分配的缓冲区,是建立在 物理内存上的,在某种情况下是可以提高效率的。
    如图:


    2.PNG

    (直接缓冲区是将原来的“copy”部分换成“物理内存映射文件” , 当用户程序写数据的时候,就将该数据写到这个 映射文件中,之后物理磁盘直接从这个物理磁盘获取数据,反之用户程序也可以直接从映射文件中读取数据。也就少了“copy”的开销)

    所以“直接缓冲区”比“非直接缓冲区”效率要高

    (二)直接缓冲区的缺点
    当然,直接缓冲区也因为这种方式带来了一些缺点:
    1、不安全
    2、资源消耗比较大
    3、当写入映射文件后,用户程序就不能够管理已经写入的数据了,其“分配”和“销毁”操作由操作系统决定(这里之前我以为是jvm虚拟机,,映射文件在直接内存,但是直接内存是用的本机内存(其实这里有点模糊,所以有其他见解一起交流哇))。

    关于直接缓冲区与非直接缓冲区,官方API文档有如下简述:


    3.PNG

    四、通道Channel的原理与获取

    早期的IO操作,当多个用户程序需要读写一个数据时,或者说多个IO操作,经过直接缓冲区或者非直接缓冲区,到达物理磁盘时,单个CPU操作这多个IO,然后再写入物理磁盘,最后写入内存。对于多并发IO来说,性能是非常不友好的。
    如图:


    1.PNG

    于是就有了直接连接在内存与IO操作之间的“DMA总线” , 当有一个IO操作进来时,经过CPU的认证,建立一个直接连接内存和IO操作的总线,之后的IO读写就可以通过这个“DMA”总线直接读写数据到内存。
    如图:


    2.PNG
    但是后来发现,这样仍然会对CPU性能不怎么友好,因为每一次IO操作都会判断。于是就有了“通道Channel” ,这个通道专门用于 IO操作, 就无需CPU判断。这种“直接的”“专门的”方式也就减轻了CPU很大的负担。
    如图:
    3.PNG

    -------------------------------------Channel的基本操作-------------------------------------

    1、概念:用于源节点与目标节点的连接,在java NIO中负责缓冲区中数据的传输,Channel本身不存储数据,因此需要配合缓冲区进行传输。
    2、通道的主要实现类
    java.nio.channels.Channel 接口:
    |---------FileChannel
    |---------SocketChannel
    |---------ServerSocketChannel
    |---------DatagramChannel
    3、获取通道
    |---------getChannel()方法
    本地IO: FileInputStream / FileOutputStream 、RundomAccessFile
    网络IO: Socket 、ServerSocket、DatagramSocket
    |---------JDK 1.7 中的NIO .2针对各个通道提供了静态方法 open() ;
    |---------JDK 1.7 中的NIO .2的Files 工具类的newByteChannel();

    五、NIO 通道数据传输:直接缓冲与非直接缓冲的比较

    代码demo:

    /**
     * @Author : WJ
     * @Date : 2018/12/10/010 21:39
     * <p>
     * 注释: 利用通道,基于“缓冲区”的方式进行文件复制
     */
    public class Test {
    
    /************************************非直接缓冲区方式************************************/
    
        /**
         *
         * 效率较直接缓冲区方式“慢”,但相对“稳定”,对于IO操作不频繁以及IO连接时间不长的可以选用此方式。
         */
        @org.junit.Test
        public void test1() throws IOException {
            //建立流
            FileInputStream inputStream = new FileInputStream("1.jpg");
            FileOutputStream outputStream = new FileOutputStream("2.jpg");
    
            //获取通道
            FileChannel inChannel = inputStream.getChannel();
            FileChannel outChannel = outputStream.getChannel();
    
            //分配指定大小的“非直接缓冲区”
            ByteBuffer buf = ByteBuffer.allocate(1024);
    
            //将通道中的数据存入缓冲区中
            while (inChannel.read(buf)!= -1){
                //切换到读取数据的模式
                buf.flip();
                //将缓冲区中的数据写入缓冲区中
                outChannel.write(buf);
                //清空缓冲区
                buf.clear();
            }
            //最后关闭流和缓冲区
            outChannel.close();
            inChannel.close();
            outputStream.close();
            inputStream.close();
        }
    
    /*****************************************直接缓冲区方式************************************/
        /**
         *
         * 通过“内存映射文件”方式复制数据
         *
         * 此种方式效率会很快,但是有些不稳定,有的时候,文件已经读写完成了,但是程序依旧在执行中:
         * 这是因为当用户程序将数据写入内存映射文件中后,程序与数据就无关了,这个时候JVM的垃圾收集机制可能还没来得及回收用户程序
         * 数据就已经读写完成了。
         *
         * 适用于: 长时间进行IO连接操作,大量IO操作等情况。
         *
         */
        public void test2()throws IOException{
            /**
             * 获取通道
             * 这里有两个工具类:Paths 和 StandardOpenOption
             *
             * Paths 可以直接指定数据路径,也可多个字符串拼接路径
             *
             * StandardOpenOption.READ:允许读
             * StandardOpenOption.WRITE:允许写
             * StandardOpenOption.CREATE:允许创建(如果存在那么覆盖)
             * StandardOpenOption.CREATE_NEW: 允许创建(如果存在则报错)
             */
            FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
            FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
    
            //内存映射文件
            MappedByteBuffer inMappedByteBuf = inChannel.map(FileChannel.MapMode.READ_ONLY
                    ,0,inChannel.size());
            MappedByteBuffer outMappedByteBuf = outChannel.map(FileChannel.MapMode.READ_WRITE
                    ,0,inChannel.size());
    
            //直接对缓冲区进行数据的读写操作
            byte[] temp = new byte[inMappedByteBuf.limit()];
            //读到内存映射文件中
            inMappedByteBuf.get(temp);
            //写到物理磁盘中去
            outMappedByteBuf.put(temp);
    
        }
    
        /**
         * 当然直接缓冲还有一种更为直接的使用方法
         *
         * 情况与上相同
         *
         */
        public void test3() throws IOException{
            //首先获取通道的方式不变
            FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"),StandardOpenOption.READ);
            FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"),StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
    
            //读写数据
            inChannel.transferTo(0,inChannel.size(),outChannel);
            //或者
            outChannel.transferFrom(inChannel,0,inChannel.size());
    
            /**上面两种没有什么区别:也就是从哪来,或者到哪去*/
    
            //关闭通道
            outChannel.close();
            inChannel.close();
        }
    }
    

    六、分散读取与聚集写入

    代码demo:

      /*********************************分散读取与聚集写入************************************/
        /**
         * 分散读取:一次从一个通道 读取多个 缓冲区
         * 聚集写入;一个写入多个缓冲区 到一个通道
         * @throws IOException
         */
        public void test4() throws IOException{
            RandomAccessFile randomAccessFile = new  RandomAccessFile("文件路径","rw");
            //获取通道
            FileChannel channel = randomAccessFile.getChannel();
            //分配指定大小的缓冲区若干个
            ByteBuffer byteBuffer1 = ByteBuffer.allocate(100);
            ByteBuffer byteBuffer2 = ByteBuffer.allocate(200);
            ByteBuffer byteBuffer3 = ByteBuffer.allocate(1024);
    
            /***************分散读取*********************/
            ByteBuffer[] buffers = {byteBuffer1,byteBuffer2,byteBuffer3};
            channel.read(buffers);
    
            /***************聚集写入*********************/
            RandomAccessFile randomAccessFile1 =  new RandomAccessFile("文件路径","rw");
            FileChannel channel1 = randomAccessFile1.getChannel();
    
            channel1.write(buffers);
    
        }
    

    七、NIO阻塞与非阻塞

    (一)阻塞:

    在传统IO单线程处理模式中,客户端发起一个读写请求,如果这个请求不是真实有效的,那么将会造成阻塞,后面的线程无法进来,也就造成了性能的急剧下降。

    原来我们解决这个问题是采用 “多线程的IO”:
    将用户请求分配到多个线程中,当一个线程被阻塞后,其他线程仍然可以继续请求。这也是多线程的优点。

    但是,那些被阻塞的线程,那个线程后面的仍然无法请求,所以这种方式也不是最好的解决方案。

    所以上面两种就是“传统IO”阻塞的缺陷。

    (二)非阻塞

    在客户端与服务端之间,有一个“Selector选择器” ,这个选择器时刻将所有 来自客户端的“通道(Channel)” 进行判断是否准备就绪,当通道准备就绪,选择器就 将 这个 通道,也就是客户端发过来的请求任务 分配到一个或者多个线程上,在此之前,服务端可以 自己完成自己的事情,这样也就 增加了CPU的利用。


    以下于2019年5月3日更新

    八、Buffer的相关操作

    1、buffer的创建

    //buffer的创建
    ByteBuffer   bu = ByteBuffer.allocate(1024);
    //从既有数组中创建
    byte   array[] = new byte[1024];
    ByteBuffer  bu = ByteBuffer.wrap(array);
    

    2、重置和清空缓冲区
    ①rewind()将position置零,并清除标志位
    ②clear()也将position置零,同时将limit设置为capacity大小,也就是回到最初的样子。
    ③flip()先将limit设置到positon所在位置,然后将positon置零,并清除标志位,这是应用于读写操作时的转换。

    3、标志缓冲区
    mark()方法,标记当前位置为标志位,当下次调用reset()方法时将会使position回到此位置。

    4、复制缓冲区
    duplicate():生成一个完全一样的缓冲区,读写互不干扰

    5、缓冲区分片
    slice():从现有的缓冲区中,创建新的子缓冲区,子缓冲区和父缓冲区共享数据。

    6、只读缓冲区
    asReadOnlyBuffer():得到一个与当前缓冲区一致的,并且共享内存数据的只读缓冲区。只读缓冲区只能被读,不能被写,当前缓冲区修改,会同步到只读缓冲区。

    7、文件映射到内存
    比基于流的方式要快得多,代码创建如下:

    MappedByteBuffer   mbb = fc.map(FileChannel.MapMode.READ_WRITE,0,1024);
    

    以上代码是将文件的前1024个字节映射到内存中,mao()方法返回一个MappedByteBuffer。

    相关文章

      网友评论

          本文标题:“IO与NIO ”重点概念整理

          本文链接:https://www.haomeiwen.com/subject/tdfrhqtx.html