“IO与NIO ”重点概念整理

2018-12-12  本文已影响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。

上一篇下一篇

猜你喜欢

热点阅读