• java高级之NIO


    BIO、NIO、AIO区别

    • BIOBlock IO 同步阻塞式 IO,在传统的java.io包下,它基于流模型实现(面向的IO操作),提供了我们最熟知的一些IO功能,例如File 抽象、输入输出流等。BIO的交互方式是同步、阻塞的方式,即在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。

    • NIOnon-blocking IO 同步非阻塞 IO,是在JDK1.4中引入的NIO框架(java.nio包),可以看作是传统IO的升级,NIO支持面向缓冲区的、基于通道的IO操作。NIO提供了SelectorChannelBuffer 等新的抽象,可以构建多路复用的、同步非阻塞IO程序,提供了更接近操作系统底层的高性能数据操作方式。

    • AIOAsynchronous IO 是 NIO 的升级,在JDK1.7中实现,也叫 NIO2,实现了异步非堵塞IO,异步IO的操作基于事件和回调机制。

    NIO知识整理

    • NIO主要有三大核心部分:Selector(选择器)、Channel(通道)、Buffer(缓冲区);
    • NIO是面向缓冲区,面向块的编程,数据读取到一个稍后会处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用NIO可以提供非阻塞式的高伸缩性网络。

    NIO与IO区别

    IO(同步阻塞式 IO) NIO(同步非阻塞 IO)
    面向流 面向缓冲区
    阻塞式 非阻塞式
    选择器、通道
    • IO是面向流的,流是单向的,比如从文件(磁盘、网络)到程序的过程中使用的输入输出流都是单向的。
    • NIO是面向缓冲区的,NIO在文件(磁盘、网络)和程序之间建立通道(Channel),传输的数据通过缓冲区进行存取,缓冲区在通道中进行传递运输,例如火车与铁轨的关系,是双向的。

    通道与缓冲区

    • 通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
    • Channel 负责传输、连接, Buffer 负责数据存储、操作。
    • 缓冲区( Buffer):一个用于特定基本数据类型的容器。它主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。

    缓冲区Buffer

    • java.nio 包定义,所有缓冲区都是 Buffer 抽象类的子类。常用子类如下:

      image-20201022223505943

    • Buffer中的四个核心属性:

      • 标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
      public abstract class Buffer {
        	//标记 (mark)与重置 (reset): 标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position。
          private int mark = -1;
        	//下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
          private int position = 0;
        	//限制 (limit): 第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
          private int limit;
        	//容量 (capacity) : 表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
          private int capacity;
      }
      

      image-20201022224126511

    • Buffer 的常用方法

      返回值 方 法
      Buffer clear() 清空缓冲区并返回对缓冲区的引用
      Buffer flip() 翻转缓冲区,读写切换
      int capacity() 返回 Buffer 的 capacity 大小
      boolean hasRemaining() 判断缓冲区中是否还有元素
      int limit() 返回 Buffer 的界限(limit) 的位置
      Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
      Buffer mark() 对缓冲区设置标记
      int position() 返回缓冲区的当前位置 position
      Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
      int remaining() 返回 position 和 limit 之间的元素个数
      Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
      Buffer rewind() 将位置设为为 0, 取消设置的 mark
    • Buffer的子类常用方法,注意字节缓冲区中的直接缓冲区非直接缓冲区的区别。

      • 直接缓冲区可以通过allocateDirect()FileChannelmap() 方法来创建,返回值为MappedByteBuffer
      方法的返回值 方法名 描述
      static ****Buffe allocate(int capacity) 分配一个新的**缓冲区。
      static ByteBuffer allocateDirect(int capacity) 分配一个新的直接字节缓冲区。
      abstract *** get() 获取缓冲区的数据,多种类型的重载
      abstract ***Buffer put(***) 存入数据到缓冲区 重载
    • 代码练习

      @Test
      public void testBufer(){
          String str = "Practice Buffer";
          //1、创建一个字节缓冲区 分配大小为128
          ByteBuffer byteBuffer = ByteBuffer.allocate(128);
          //2、核心属性  0 <= mark <= position <= limit <= capacity
          //  2.1拿到容量
          System.out.println("容量为:"+byteBuffer.capacity());
          //  2.2拿到限制
          System.out.println("限制为:"+byteBuffer.limit());
          //  2.3拿到位置
          System.out.println("当前位置为:"+byteBuffer.position());
          //3、put() 将数据写入缓冲区
          byteBuffer.put(str.getBytes());
          //4、flip() 切换读写模式
          byteBuffer.flip();
          //5、get()  读取数据
          byte[] bytes = new byte[byteBuffer.limit()];
          byteBuffer.get(bytes);
          System.out.println(new String(bytes,0,bytes.length));
          //6、rewind() 可重复读  将位置设为为 0
          byteBuffer.rewind();
          //7、读两个位置的数据
          byte[] dst = new byte[byteBuffer.limit()];
          byteBuffer.get(dst, 0, 2);
          System.out.println(new String(dst, 0, 2));
          //拿到位置和限制
          System.out.println("当前位置为:"+byteBuffer.position()+",当前限制为"+byteBuffer.limit());
          //8、核心属性标记  mark
          byteBuffer.mark();
          //9、在读两个字节的数据
          byteBuffer.get(dst,2,2);
          //拿到位置和限制
          System.out.println("当前位置为:"+byteBuffer.position()+",当前限制为"+byteBuffer.limit());
          //10、reset() 恢复到 标记mark位置
          byteBuffer.reset();
          //拿到位置和限制
          System.out.println("当前位置为:"+byteBuffer.position()+",当前限制为"+byteBuffer.limit());
          //11、hasRemaining() 判断缓冲区是否还有元素
          if(byteBuffer.hasRemaining()){
              //12、可以操作的数量  返回 position 和 limit 之间的元素个数
              System.out.println(byteBuffer.remaining());
          }
          //取消设置的 mark
          byteBuffer.rewind();
          //13、clear(); 清空缓冲区  但是缓冲区中数据仍然存在
          byteBuffer.clear();
          System.out.println((char)byteBuffer.get());
          //14、分配直接缓冲区
          ByteBuffer buf = ByteBuffer.allocateDirect(1024);
          //判断字节缓冲区是直接还是非直接
          System.out.println(buf.isDirect());
      }
      

    通道(Channel)

    • java.nio.channels 包中定义,它表示 IO 源与目标打开的连接。可以将其类比于传统的“流”。但Channel本身不能直接访问数据, 它只能与Buffer进行交互。

    • 获取通道

      • 可以对支持通道的对象调用getChannel() 方法。支持通道的类有:本地IO为FileInputStreamFileOutputStreamRandomAccessFile ,网络IO为DatagramSocketSocketServerSocket
      • 在NIO2中,通过通道的静态方法 open() 打开并返回指定通道;
      • 在NIO2中,使用 Files 类的静态方法 newByteChannel() 获取字节通道。
    /**
     * 1、FileChannel 的open()方法  作用打开或创建文件,返回文件通道以访问该文件。
     * 2、参数:path - 打开或创建文件的路径   options - 指定文件打开方式的选项
     * 3、OpenOption 使用StandardOpenOption枚举类指定
     *      APPEND:如果文件打开 WRITE访问,则字节将被写入文件的末尾而不是开头。
     *      CREATE:创建一个新文件(如果不存在)。
     *      CREATE_NEW:创建一个新的文件,如果该文件已经存在失败。
     *      DELETE_ON_CLOSE:关闭时删除。
     *      DSYNC:要求将文件内容的每次更新都与底层存储设备同步写入。
     *      READ:打开阅读权限。
     *      SPARSE:稀疏文件
     *      SYNC:要求将文件内容或元数据的每次更新都同步写入底层存储设备。
     *      TRUNCATE_EXISTING:如果文件已经存在,并且打开 WRITE访问,则其长度将截断为0。
     *      WRITE:打开以进行写入。
     */
    public static FileChannel open(Path path, OpenOption... options) throws IOException{
        Set<OpenOption> set = new HashSet<OpenOption>(options.length);
        Collections.addAll(set, options);
        return open(path, set, NO_ATTRIBUTES);
    }
    
    • 可以利用通道完成整个数据传输,不使用缓冲区,使用通道的transferFrom()transferTo()方法。

    • 通道的分散(Scatter)和聚集(Gather):

      • 分散读取( Scattering Reads)是指从 Channel 中读取的数据“分散” 到多个 Buffer 中。
      • 聚集写入( Gathering Writes)是指将多个 Buffer 中的数据“聚集”到 Channel
    • 通道的常用方法

      方 法 描 述
      int read(ByteBuffer dst) 从 Channel 中读取数据到 ByteBuffer
      long read(ByteBuffer[] dsts) 将 Channel 中的数据“分散”到 ByteBuffer[]
      int write(ByteBuffer src) 将 ByteBuffer 中的数据写入到 Channel
      long write(ByteBuffer[] srcs) 将 ByteBuffer[] 中的数据“聚集”到 Channel
      long position() 返回此通道的文件位置
      FileChannel position(long p) 设置此通道的文件位置
      long size() 返回此通道的文件的当前大小
      FileChannel truncate(long s) 将此通道的文件截取为给定大小
      void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中
    • 代码练习

    //为了简化 未做异常处理
    @Test
    public void testChannel() throws IOException {
        //一、使用非直接缓冲区完成文件复制
        //1、创建文件输入流
        FileInputStream fileInputStream = new FileInputStream("爱情与友情.jpg");
        //2、创建文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream("爱情与友情6.jpg");
        //3、fileInputStream 与 fileOutputStream 支持通道 获取通道
        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();
        //4、分配缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //5、将通道中数据存入缓冲区
        while (inputStreamChannel.read(byteBuffer)!=-1){
            //6、切换读写模式
            byteBuffer.flip();
            //7、将数据写入到通道中
            outputStreamChannel.write(byteBuffer);
            //8、清空缓冲区
            byteBuffer.clear();
        }
    //*********************************************************************************//
        //二、使用直接缓冲区完成文件复制 通道的静态方法 open() 打开并返回指定通道
        //1、创建通道
        FileChannel inChannle = FileChannel.open(Paths.get("爱情与友情.jpg"), StandardOpenOption.READ);
        FileChannel outChannle = FileChannel.open(Paths.get("爱情与友情7.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        //2、直接缓冲区,内存映射文件
        // 此频道文件的区域直接映射到内存中。 只读:READ_ONLY  读写:READ_WRITE  私有:PRIVATE 
        MappedByteBuffer inMappedByteBuffer = inChannle.map(FileChannel.MapMode.READ_ONLY, 0, inChannle.size());
        MappedByteBuffer outMappedByteBuffer = outChannle.map(FileChannel.MapMode.READ_WRITE, 0, inChannle.size());
        byte[] bytes = new byte[inMappedByteBuffer.limit()];
        inMappedByteBuffer.get(bytes);
        outMappedByteBuffer.put(bytes);
        inChannle.close();
        outChannle.close();       //*********************************************************************************//
        //三、通道之间的数据传输
        FileChannel inChannle1 = FileChannel.open(Paths.get("爱情与友情.jpg"), StandardOpenOption.READ);
        FileChannel outChannle1 = FileChannel.open(Paths.get("爱情与友情8.jpg"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        //inChannle1.transferTo(0,inChannle.size(),outChannle1);
        outChannle1.transferFrom(inChannle1,0,inChannle1.size());
        inChannle1.close();
        outChannle1.close();
    //*********************************************************************************//
        //四、分散和聚集
        //四-1 分散读取
        //1、创建一个随机存取文件流
        RandomAccessFile randomAccessFile = new RandomAccessFile("hello1.txt", "rw");
        //2、获取通道
        FileChannel channel = randomAccessFile.getChannel();
        //3、获取缓冲区
        ByteBuffer byteBuffer1 = ByteBuffer.allocate(10);
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(100);
        //4、分散读取
        ByteBuffer[] byteBuffers = {byteBuffer1,byteBuffer2};
        //5、将通道中数据分散到buffer中
        channel.read(byteBuffers);
        for (ByteBuffer byteBuffer3 : byteBuffers) {
            //6、读写切换
            byteBuffer3.flip();
        }
        //7、查看结果
        System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
        System.out.println("===================================");
        System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));
        //四-2 聚集写入
        RandomAccessFile raf2 = new RandomAccessFile("hello5.txt", "rw");
        FileChannel channel2 = raf2.getChannel();
        channel2.write(byteBuffers);
    }
    

    选择器

    • 传统的阻塞IO方式在数据被读取或写入时,该线程在此期间不能执行其他任务。而NIO的非阻塞方式在没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。

    • 选择器Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读、写以及接收事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。也就是说选择器监控这些通道的IO状况(连接接入、读、写以及接收事件)。

    • 选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 ChannelSelector 是非阻塞 IO 的核心。

    • SelectableChannle是可通过Selector复用的通道,它是所有支持就绪检查的通道类的父类,提供了实现通道的可选择性所需要的公共方法。注意:FileChannel类没有继承SelectableChannel因此不是可选通道。

      image-20201108163315681

    • 选择键(SelectionKey):选择键封装了特定的通道SelectableChannel特定的选择器Selector的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。

    • 注册方法详解,第二个参数可以理解为选择器对通道的监听事件。多个监听事件时可以使用位或‘|’连接。

      //sel - 要注册该频道的选择器  ops - 为结果键设置的兴趣
      //SelectionKey中有四个事件:OP_CONNECT、OP_ACCEPT、OP_READ、OP_WRITE
      public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException
      
    • 代码实例

      • 阻塞式IO
    //阻塞式IO
    @Test
    public void testClient1() throws IOException {
        //1、获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
        //2、获取文件通道
        FileChannel fileChannel = FileChannel.open(Paths.get("爱情与友情.jpg"), StandardOpenOption.READ);
        //3、创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //4、读取本地文件,发送到服务器
        while (fileChannel.read(byteBuffer) != -1){
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
        }
        //5、关闭连接以进行写入,而不关闭通道。
        socketChannel.shutdownOutput();
        //6、接收服务器的反馈
        int len = 0;
        while ((len = socketChannel.read(byteBuffer)) != -1){
            //7、读写切换
            byteBuffer.flip();
            System.out.println(new String(byteBuffer.array(),0,len));
            //8、清除缓存
            byteBuffer.clear();
        }
        //9、关闭通道
        socketChannel.close();
        fileChannel.close();;
    }
    
    @Test
    public void testServer1() throws IOException {
        //1、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2、打开文件通道
        FileChannel fileChannel = FileChannel.open(Paths.get("爱情与友情9.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        //3、将通道的套接字绑定到本地地址,并配置套接字以监听连接。
        serverSocketChannel.bind(new InetSocketAddress(9999));
        //4、接收客户端的连接
        SocketChannel socketChannel = serverSocketChannel.accept();
        //5、创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //6、读取数据并写入
        while(socketChannel.read(byteBuffer) != -1){
            byteBuffer.flip();
            fileChannel.write(byteBuffer);
            byteBuffer.clear();
        }
        //7、发送数据到客户端
        byteBuffer.put("我是服务端,我已经成功接收到数据".getBytes());
        byteBuffer.flip();
        //8、将数据写入到通道
        socketChannel.write(byteBuffer);
        //9、关闭通道
        socketChannel.close();
        fileChannel.close();
        serverSocketChannel.close();
    }
    
    • 非阻塞式IO
    //非阻塞式IO
    @Test
    public void testclient2() throws IOException {
        //1、获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));
        //2、切换非阻塞模式
        socketChannel.configureBlocking(false);
        //3、分配缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //4、发送数据给服务端
        byteBuffer.put(("客户端传送时间数据:" + new Date()).getBytes());
        byteBuffer.flip();
        socketChannel.write(byteBuffer);
        byteBuffer.clear();
        //5、关闭通道
        socketChannel.close();
    }
    @Test
    public void testServer2() throws IOException {
        //1. 获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //2. 切换非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //3. 绑定连接
        serverSocketChannel.bind(new InetSocketAddress(8888));
        //4. 获取选择器
        Selector selector = Selector.open();
        //5. 将通道注册到选择器上, 并且指定“监听接收事件”
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6. 轮询式的获取选择器上已经“准备就绪”的事件
        while (selector.select()>0){
            //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                //8. 获取准备“就绪”的事件
                SelectionKey selectionKey = iterator.next();
                //9. 判断具体是什么事件准备就绪
                if(selectionKey.isAcceptable()){
                    //10. 若“接收就绪”,获取客户端连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //11. 切换非阻塞模式
                    socketChannel.configureBlocking(false);
                    //12. 将该通道注册到选择器上
                    socketChannel.register(selector,SelectionKey.OP_READ);
                } else if(selectionKey.isReadable()){
                    //13. 获取当前选择器上“读就绪”状态的通道
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    //14. 创建缓冲区 读取数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = socketChannel.read(byteBuffer))>0){
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(),0,len));
                        byteBuffer.clear();
                    }
                }
                //15. 取消选择键 SelectionKey
                iterator.remove();
            }
        }
    }
    

    NIO2知识整理

    • 新增Path接口,Paths工具类,Files工具类。 这些接口和工具类对NIO中的功能进行了高度封装,大大简化了文件系统的IO编程。
    • java.nio.file.Path 接口代表一个平台无关的平台路径,描述了目录结构中文件的位置。
    • java.nio.file.Paths仅由静态方法组成,通过转换路径字符串返回Path或URI 。
    • java.nio.file.Files 用于操作文件或目录的工具类。

    欢迎关注
    公众号三筒记简介:分享各种编程知识、excel相关技巧、读书笔记

  • 相关阅读:
    kubernetes
    dubbo
    前端控件+资源
    Gossip
    问题解决1
    react
    impala
    storm+Calcite
    ASP.NET页面传值与跳转
    经典FormsAuthenticationTicket 分析
  • 原文地址:https://www.cnblogs.com/manongxiao/p/14311139.html
Copyright © 2020-2023  润新知