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相关技巧、读书笔记

原文地址:https://www.cnblogs.com/manongxiao/p/14311139.html