0
点赞
收藏
分享

微信扫一扫

nio---io区别 20230403

文件的抽象化表示,字节流以及字符流的文件操作等属于传统 IO 的相关内容,我们已经在前面的文章进行了较为深刻的学习了。

但是传统的 IO 流还是有很多缺陷的,尤其它的阻塞性加上磁盘读写本来就慢,会导致 CPU 使用效率大大降低。

所以,jdk 1.4 发布了 NIO 包,NIO 的文件读写设计颠覆了传统 IO 的设计,采用『通道』+『缓存区』使得新式的 IO 操作直接面向缓存区,并且是非阻塞的,对于效率的提升真不是一点两点,我们一起来看看。

通道 Channel

我们说过,NIO 的核心就是通道和缓存区,所以它们的工作模式是这样的:

nio---io区别 20230403_缓存

通道有点类似 IO 中的流,但不同的是,同一个通道既允许读也允许写,而任意一个流要么是读流要么是写流。

但是你要明白一点,通道和流一样都是需要基于物理文件的,而每个流或者通道都通过文件指针操作文件,这里说的「通道是双向的」也是有前提的,那就是通道基于随机访问文件『RandomAccessFile』的可读可写文件指针。

『RandomAccessFile』是既可读又可写的,所以基于它的通道是双向的,所以,「通道是双向的」这句话是有前提的,不能断章取义。

基本的通道类型有如下一些:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel 是基于文件的通道,SocketChannel 和 ServerSocketChannel 用于网络 TCP 套接字数据报读写,DatagramChannel 是用于网络 UDP 套接字数据报读写。

通道不能单独存在,它永远需要绑定一个缓存区,所有的数据只会存在于缓存区中,无论你是写或是读,必然是缓存区通过通道到达磁盘文件,或是磁盘文件通过通道到达缓存区。

即缓存区是数据的「起点」,也是「终点」,具体这些通道到底有哪些不同以及该如何使用,基本实现如何,我们介绍完『缓存区』概念后,再做详细学习。

缓存区 Buffer

Buffer 是所有具体缓存区的基类,是一个抽象类,它的实现类有很多,包含各种类型数据的缓存。

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • MappedByteBuffer

我们以 ByteBuffer 为例进行学习,其余的缓存区也都是基于字节缓存区的,只不过多了一步字节转换过程而已,MappedByteBuffer 是一个特殊的缓存方式,我们会单独介绍。

Buffer 中有几个重要的成员属性,我们了解一下:

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
long address;

mark 属性我们已经不陌生了,用于重复读。capacity 描述缓存区容量,即整个缓存区最大能存储多少数据量。address 用于操作直接内存,区别于 jvm 内存,这一点待会说明。

而 position 和 limit 我想用一张图结合解释:

nio---io区别 20230403_选择器_02

由于缓存区是读写共存的,所以不同的模式下,这两个变量的值也具有不同的意义。

写模式下,所谓写模式就是将缓存区中的内容写入通道。position 代表下一个字节应该被写出去的字节在缓存区中的位置,limit 表示最后一个待写字节在缓存区的位置。

读模式下,所谓读模式就是从通道读取数据到缓存区。position 代表下一个读出来的字节应当存储在缓存区的位置,limit 等于 capacity。

相关的读写操作细节,待会会和大家一起看源码,以加深对通道和缓存区协作工作的原理,这里我们先讨论一个大家可能没怎么关注过的一个问题。

JVM 内存划分为栈和堆,这是大家深入脑海的知识,但是其实划分给 JVM 的还有一块堆外内存,也就是直接内存,很多人不知道这块内存是干什么用的。

这是一块物理内存,专门用于 JVM 和 IO 设备打交道,Java 底层使用 C 语言的 API 调用操作系统与 IO 设备进行交互。

例如,Java 内存中有一个字节数组,现在调用流将它写入磁盘文件,那么 JVM 首先会将这个字节数组先拷贝一份到堆外内存中,然后调用 C 语言 API 指明将某个连续地址范围的数据写入磁盘。

读操作也是类似,而 JVM 额外做的拷贝工作也是有意义的,因为 JVM 是基于自动垃圾回收机制运行的,所有内存中的数据会在 GC 时不停的被移动,如果你调用系统 API 告诉操作系统将内存某某位置的内存写入磁盘,而此时发生 GC 移动了该部分数据,GC 结束后操作系统是不是就写错数据了。

所以,JVM 对于与外围 IO 设备交互的情况下,都会将内存数据复制一份到堆外内存中,然后调用系统 API 间接的写入磁盘,读也是类似的。由于堆外内存不受 GC 管理,所以用完一定得记得释放。

理解这一个小知识是看懂源码实现的前提,不然你可能不知道代码实现者在做什么。好了,那我们就先来看看读操作的基本使用与源码实现。

RandomAccessFile file = new RandomAccessFile
        ("C:\\Users\\yanga\\Desktop\\note.txt","rw");
FileChannel channel = file.getChannel();

ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);

buffer.flip();
byte[] res = new byte[1024];
buffer.get(res,0,buffer.limit());
System.out.println(new String(res));

channel.close();

我们看这么一段代码,这段代码我大致分成了四个部分,第一部分用于获取文件通道,第二部分用于分配缓存区并完成读操作,第三部分用于将缓存区中数据进行打印,第四部分为关闭通道连接。

第一部分:

getChannel 方法用于获取一个文件相关的通道实例,具体实现如下:

public final FileChannel getChannel() {
    synchronized (this) {
        if (channel == null) {
            channel = FileChannelImpl.open(fd, path, true, rw, this);
        }
        return channel;
    }
}

public static FileChannel open
(FileDescriptor var0, String var1, boolean var2, boolean var3, Object var4) {
    return new FileChannelImpl(var0, var1, var2, var3, false, var4);
}

getChannel 方法会调用 FileChannelImpl 的工厂方法构建一个 FileChannelImpl 实例,FileChannelImpl 是抽象类 FileChannel 的一个子类实现。

构成 FileChannelImpl 实例所需的必要参数有,该文件的文件指针,该文件的完整路径,读写权限等。

第二部分:

Buffer 的基本结构我们上述已经简单介绍了,这里不再赘述了,所谓的缓存区,本质上就是字节数组。

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

ByteBuffer 实例的构建是通过工厂模式产生的,必须指定参数 capacity 作为内部字节数组的容量。HeapByteBuffer 是虚拟机的堆上内存,所有数据都将存储在堆空间,我们不久将会介绍它的一个兄弟,DirectByteBuffer,它被分配在堆外内存中,具体的一会说。

这个 HeapByteBuffer 的构造情况我们不妨跟进去看看:

HeapByteBuffer(int cap, int lim) {
    super(-1, 0, lim, cap, new byte[cap], 0);
}

调用父类的构造方法,初始化我们在 ByteBuffer 中提过的一些属性值,如 position,capacity,mark,limit,offset 以及字节数组 hb。

接着,我们看看这个 read 方法的调用链。

nio---io区别 20230403_数据_03

这个 read 方法是子类 FileChannelImpl 对父类 FileChannel read 方法的重写。这个方法不是读操作的核心,我们简单概括一下,该方法首先会拿到当前通道实例的锁,如果没有被其他线程占有,那么占有该锁,并调用 IOUtil 的 read 方法。

nio---io区别 20230403_选择器_04

IOUtil 的 read 方法内部也调用了很多方法,有的甚至是本地方法,这里只简单介绍一下整个 read 方法的大体逻辑,具体细节留待大家自行学习。

首先判断我们的 ByteBuffer 实例是不是一个 DirectBuffer,也就是判断当前的 ByteBuffer 实例是不是被分配在直接内存中,如果是,那么将调用 readIntoNativeBuffer 方法从磁盘读取数据直接放入 ByteBuffer 实例所在的直接内存中。

否则,虚拟机将在直接内存区域分配一块内存,该内存区域的首地址存储在 var5 实例的 address 属性中。

接着从磁盘读取数据放入 var5 所代表的直接内存区域中。

最后,put 方法会将 var5 所代表的直接内存区域中的数据写入到 var1 所代表的堆内缓存区并释放临时创建的直接内存空间。

这样,我们传入的缓存区中就成功的被读入了数据。写操作是相反的,大家可以自行类比,反正堆内数据想要到达磁盘就必定要经过堆外内存的复制过程。

第三第四部分比较简单,这里不再赘述了。提醒一下,想要更好的使用这个通道和缓存区进行文件读写操作,你就一定得对缓存区的几个变量的值时刻把握住,position 和 limit 当前的值是什么,大致什么位置,一定得清晰,否则这个读写共存的缓存区可能会让你晕头转向。

选择器 Selector

Selector 是 Java NIO 的一个组件,它用于监听多个 Channel 的各种状态,用于管理多个 Channel。但本质上由于 FileChannel 不支持注册选择器,所以 Selector 一般被认为是服务于网络套接字通道的。

而大家口中的「NIO 是非阻塞的」,准确来说,指的是网络编程中客户端与服务端连接交换数据的过程是非阻塞的。普通的文件读写依然是阻塞的,和 IO 是一样的,这一点可能很多初学者会懵,包括我当时也总想不通为什么说 NIO 的文件读写是非阻塞的,明明就是阻塞的。

nio---io区别 20230403_数据_05

创建一个选择器一般是通过 Selector 的工厂方法,Selector.open :

Selector selector = Selector.open();

而一个通道想要注册到某个选择器中,必须调整模式为非阻塞模式,例如:

//创建一个 TCP 套接字通道
SocketChannel channel = SocketChannel.open();
//调整通道为非阻塞模式
channel.configureBlocking(false);
//向选择器注册一个通道
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

以上代码是注册一个通道到选择器中的最简单版本,支持注册选择器的通道都有一个 register 方法,该方法就是用于注册当前实例通道到指定选择器的。

该方法的第一个参数就是目标选择器,第二个参数其实是一个二进制掩码,它指明当前选择器感兴趣当前通道的哪些事件。以枚举类型提供了以下几种取值:

  • int OP_READ = 1 << 0;
  • int OP_WRITE = 1 << 2;
  • int OP_CONNECT = 1 << 3;
  • int OP_ACCEPT = 1 << 4;

这种用二进制掩码来表示某些状态的机制,我们在讲述虚拟机类类文件结构的时候也遇到过,它就是用一个二进制位来描述一种状态。

register 方法会返回一个 SelectionKey 实例,该实例代表的就是选择器与通道的一个关联关系。你可以调用它的 selector 方法返回当前相关联的选择器实例,也可以调用它的 channel 方法返回当前关联关系中的通道实例。

除此之外,SelectionKey 的 readyOps 方法将返回当前选择感兴趣当前通道中事件中准备就绪的事件集合,依然返回的一个整型数值,也就是一个二进制掩码。

例如:

int readySet = selectionKey.readyOps();

假如 readySet 的值为 13,二进制 「0000 1101」,从后向前数,第一位为 1,第三位为 1,第四位为 1,那么说明选择器关联的通道,读就绪、写就绪,连接就绪。

所以,当我们注册一个通道到选择器之后,就可以通过返回的 SelectionKey 实例监听该通道的各种事件。

当然,一旦某个选择器中注册了多个通道,我们不可能一个一个的记录它们注册时返回的 SelectionKey 实例来监听通道事件,选择器应当有方法返回所有注册成功的通道相关的 SelectionKey 实例。

Set<SelectionKey> keys = selector.selectedKeys();

selectedKeys 方法会返回选择器中注册成功的所有通道的 SelectionKey 实例集合。我们通过这个集合的 SelectionKey 实例,可以得到所有通道的事件就绪情况并进行相应的处理操作。

下面我们以一个简单的客户端服务端连接通讯的实例应用一下上述理论知识:

nio---io区别 20230403_选择器_06

服务端代码:

nio---io区别 20230403_数据_07

这段小程序的运行的实际效果是这样的,客户端建立请求到服务端,待请求完全建立,客户端会去检查服务端是否有数据写回,而服务端的任务就很简单了,接受任意客户端的请求连接并为它写回一段数据。

别看整个过程很简单,但只要你有一点模糊的地方,你这个功能就不可能实现,不信你试试,尤其是加了选择器的客户端代码,更值得大家一行一行分析。提醒一点的是,大家应更多的关注于哪些方法是阻塞的,哪些是非阻塞的,这会有助于分析代码。

这其实也算一个最最简单的服务器客户端请求模型了,理解了这一点相信会有助于理解浏览器与 Web 服务器的工作原理的,这里我就不再带大家分析了,有任何不同看法的也欢迎给我留言,咱们一起学习探讨。

想必你也能发现,加了选择器的代码会复杂很多,也并不一定高效于原来的代码,这其实是因为你的功能比较简单,并不涉及大量通道处理,逻辑一旦复杂起来,选择器给你带来的好处会非常明显。

其实,NIO 中还有一块 AIO ,也就是异步 IO 并没有介绍,因为异步 IO 涉及到很多其他方面知识,这里暂时不做介绍,后续文章将单独介绍异步任务等相关内容。

文章中的所有代码、图片、文件都云存储在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

 


我花了几天去了解NIO的核心知识点,期间看了《Java 编程思想》和《疯狂Java 讲义》的nio模块。但是,会发现看完了之后还是很迷,不知道NIO这是干嘛用的,而网上的资料与书上的知识点没有很好地对应。网上的资料很多都以IO的五种模型为基础来讲解NIO,而IO这五种模型其中又涉及到了很多概念:同步/异步/阻塞/非阻塞/多路复用,而不同的人又有不同的理解方式。还有涉及到了unix的select/epoll/poll/pselect,fd这些关键字,没有相关基础的人看起来简直是天书这就导致了在初学时认为nio远不可及我在找资料的过程中也收藏了好多讲解NIO的资料,这篇文章就是以初学的角度来理解NIO。也算是我这两天看NIO的一个总结吧。希望大家可以看了之后知道什么是NIO,NIO的核心知识点是什么,会使用NIO~那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~声明:本文使用JDK1.8一、NIO的概述JDK 1.4中的java.nio.*包中引入新的Java I/O库,其目的是提高速度。实际上,“旧”的I/O包已经使用NIO重新实现过,即使我们不显式的使用NIO编程,也能从中受益。nio翻译成 no-blocking io 或者 new io 都无所谓啦,都说得通~在《Java编程思想》读到“即使我们不显式的使用NIO编程,也能从中受益”的时候,我是挺在意的,所以:我们测试一下使用NIO复制文件和传统IO复制文件的性能:im

port java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class SimpleFileTransferTest {
    private long transferFile(File source, File des) throws IOException {
        long startTime = System.currentTimeMillis();        if (!des.exists())
            des.createNewFile();        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des));        //将数据源读到的内容写入目的地--使用数组
        byte[] bytes = new byte[1024 * 1024];
        int len;
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }    private long transferFileWithNIO(File source, File des) throws IOException {
        long startTime = System.currentTimeMillis();        if (!des.exists())
            des.createNewFile();        RandomAccessFile read = new RandomAccessFile(source, "rw");
        RandomAccessFile write = new RandomAccessFile(des, "rw");        FileChannel readChannel = read.getChannel();
        FileChannel writeChannel = write.getChannel();        ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);//1M缓冲区
        while (readChannel.read(byteBuffer) > 0) {
            byteBuffer.flip();
            writeChannel.write(byteBuffer);
            byteBuffer.clear();
        }        writeChannel.close();
        readChannel.close();
        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }    public static void main(String[] args) throws IOException {
        SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest();
        File sourse = new File("F:\\电影\\[电影天堂www.dygod.cn]猜火车-cd1.rmvb");
        File des = new File("X:\\Users\\ozc\\Desktop\\io.avi");
        File nio = new File("X:\\Users\\ozc\\Desktop\\nio.avi");        long time = simpleFileTransferTest.transferFile(sourse, des);
        System.out.println(time + ":普通字节流时间");        long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio);
        System.out.println(timeNio + ":NIO时间");    }
}

我分别测试了文件大小为13M,40M,200M的:<img src="https://pic1.zhimg.com/50/v2-102b00913a607b0c14ec5364a4eceec2_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="188" data-default-watermark-src="https://picx.zhimg.com/50/v2-654af8ea8debceb4dc02201a9eb7600d_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="800" data-original="https://pic1.zhimg.com/v2-102b00913a607b0c14ec5364a4eceec2_r.jpg?source=1940ef5c"/><img src="https://picx.zhimg.com/50/v2-ec40d579decde3f9a655aacddbbdf0e5_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="186" data-default-watermark-src="https://pic1.zhimg.com/50/v2-4b9cacd3f6e5337838e07aefa7710bca_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="800" data-original="https://picx.zhimg.com/v2-ec40d579decde3f9a655aacddbbdf0e5_r.jpg?source=1940ef5c"/><img src="https://picx.zhimg.com/50/v2-44f892b938c070fd4e6b88c1e419309f_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="796" data-rawheight="265" data-default-watermark-src="https://picx.zhimg.com/50/v2-b1773a5df533077f8b3ae02b21363089_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="796" data-original="https://picx.zhimg.com/v2-44f892b938c070fd4e6b88c1e419309f_r.jpg?source=1940ef5c"/>1.1为什么要使用NIO可以看到使用过NIO重新实现过的传统IO根本不虚,在大文件下效果还比NIO要好(当然了,个人几次的测试,或许不是很准)而NIO要有一定的学习成本,也没有传统IO那么好理解。那这意味着我们可以不使用/学习NIO了吗?答案是否定的,IO操作往往在两个场景下会用到:文件IO网络IONIO的魅力:在网络中使用IO就可以体现出来了!后面会说到网络中使用NIO,不急哈~二、NIO快速入门首先我们来看看IO和NIO的区别:<img src="https://picx.zhimg.com/50/v2-ef7a975d63fbddbf7bf23014f1c236da_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="334" data-default-watermark-src="https://pic1.zhimg.com/50/v2-f4504a8ea0e54fb0afc00f8567542ec9_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="800" data-original="https://pic1.zhimg.com/v2-ef7a975d63fbddbf7bf23014f1c236da_r.jpg?source=1940ef5c"/>可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理面向流的I/O 系统一次一个字节地处理数据。一个面向块(缓冲区)的I/O系统以块的形式处理数据。NIO主要有三个核心部分组成:buffer缓冲区Channel管道Selector选择器2.1buffer缓冲区和Channel管道在NIO中并不是以流的方式来处理数据的,而是以buffer缓冲区和Channel管道配合使用来处理数据。简单理解一下:Channel管道比作成铁路,buffer缓冲区比作成火车(运载着货物)而我们的NIO就是通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理!要时刻记住:Channel不与数据打交道,它只负责运输数据。与数据打交道的是Buffer缓冲区Channel-->运输Buffer-->数据相对于传统IO而言,流是单向的。对于NIO而言,有了Channel管道这个概念,我们的读写都是双向的(铁路上的火车能从广州去北京、自然就能从北京返还到广州)!2.1.1buffer缓冲区核心要点我们来看看Buffer缓冲区有什么值得我们注意的地方。Buffer是缓冲区的抽象类:<img src="https://picx.zhimg.com/50/v2-75ac01a76ffc2fd9dd05267f128d1e22_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="305" data-default-watermark-src="https://pic1.zhimg.com/50/v2-daa00a2029fcd3e315a8fba08250a4f8_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="800" data-original="https://pic1.zhimg.com/v2-75ac01a76ffc2fd9dd05267f128d1e22_r.jpg?source=1940ef5c"/>其中ByteBuffer是用得最多的实现类(在管道中读写字节数据)。<img src="https://picx.zhimg.com/50/v2-bd5b82fe7838a13d88d1633989b06296_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="635" data-rawheight="226" data-default-watermark-src="https://picx.zhimg.com/50/v2-810643ad0fcef31188b68cd9bab0f438_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="635" data-original="https://pic1.zhimg.com/v2-bd5b82fe7838a13d88d1633989b06296_r.jpg?source=1940ef5c"/>拿到一个缓冲区我们往往会做什么?很简单,就是读取缓冲区的数据/写数据到缓冲区中。所以,缓冲区的核心方法就是:put()get()<img src="https://picx.zhimg.com/50/v2-53ab8fb02d97b19e762acc6174806a27_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="373" data-rawheight="485" data-default-watermark-src="https://pic1.zhimg.com/50/v2-16dced1368c45082059b78ae10268e4a_720w.jpg?source=1940ef5c" class="content_image" width="373"/><img src="https://picx.zhimg.com/50/v2-3cd3eca0d73d04d58d92835e4e1af569_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="318" data-rawheight="443" data-default-watermark-src="https://picx.zhimg.com/50/v2-6d29f910d2758aca115dacad4de15cfa_720w.jpg?source=1940ef5c" class="content_image" width="318"/>Buffer类维护了4个核心变量属性来提供关于其所包含的数组的信息。它们是:容量Capacity缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)上界Limit缓冲区里的数据的总数,代表了当前缓冲区中一共有多少数据。位置Position下一个要被读或写的元素的位置。Position会自动由相应的 get( )和 put( )函数更新。标记Mark一个备忘位置。用于记录上一次读写的位置。<img src="https://picx.zhimg.com/50/v2-2778e2763e68f87dab017fa8cbb5ddb5_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="549" data-rawheight="210" data-default-watermark-src="https://pica.zhimg.com/50/v2-8f49ad76a252e05c572851f1ca89e5ed_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="549" data-original="https://picx.zhimg.com/v2-2778e2763e68f87dab017fa8cbb5ddb5_r.jpg?source=1940ef5c"/>2.1.2buffer代码演示首先展示一下是如何创建缓冲区的,核心变量的值是怎么变化的。public static void main(String[] args) {

// 创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);        // 看一下初始时4个核心变量的值
        System.out.println("初始时-->limit--->"+byteBuffer.limit());
        System.out.println("初始时-->position--->"+byteBuffer.position());
        System.out.println("初始时-->capacity--->"+byteBuffer.capacity());
        System.out.println("初始时-->mark--->" + byteBuffer.mark());        System.out.println("--------------------------------------");
        // 添加一些数据到缓冲区中
        String s = "Java3y";
        byteBuffer.put(s.getBytes());        // 看一下初始时4个核心变量的值
        System.out.println("put完之后-->limit--->"+byteBuffer.limit());
        System.out.println("put完之后-->position--->"+byteBuffer.position());
        System.out.println("put完之后-->capacity--->"+byteBuffer.capacity());
        System.out.println("put完之后-->mark--->" + byteBuffer.mark());
    }运行结果:<img src="https://picx.zhimg.com/50/v2-342b9eaa29a9eb4b17e8f3a32c3dc3ad_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="214" data-default-watermark-src="https://picx.zhimg.com/50/v2-664b15f860c6b7b7f82d343918062938_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="800" data-original="https://picx.zhimg.com/v2-342b9eaa29a9eb4b17e8f3a32c3dc3ad_r.jpg?source=1940ef5c"/>现在我想要从缓存区拿数据,怎么拿呀??NIO给了我们一个flip()方法。这个方法可以改动position和limit的位置!还是上面的代码,我们flip()一下后,再看看4个核心属性的值会发生什么变化:<img src="https://picx.zhimg.com/50/v2-a3cbd8571aeeadb31b78340d681bf6f4_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="800" data-rawheight="217" data-default-watermark-src="https://picx.zhimg.com/50/v2-f7b8f52e4d059c9b8f03b3e8c40c8f67_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="800" data-original="https://picx.zhimg.com/v2-a3cbd8571aeeadb31b78340d681bf6f4_r.jpg?source=1940ef5c"/>很明显的是:limit变成了position的位置了而position变成了0看到这里的同学可能就会想到了:当调用完filp()时:limit是限制读到哪里,而position是从哪里读一般我们称filp()为“切换成读模式”每当要从缓存区的时候读取数据时,就调用filp()“切换成读模式”。<img src="https://pica.zhimg.com/50/v2-228c5b3548e521e8a45b4fd4ffde9e89_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="531" data-rawheight="340" data-default-watermark-src="https://picx.zhimg.com/50/v2-4df257c5500bf2f30dca8194adcce829_720w.jpg?source=1940ef5c" class="origin_image zh-lightbox-thumb" width="531" data-original="https://picx.zhimg.com/v2-228c5b3548e521e8a45b4fd4ffde9e89_r.jpg?source=1940ef5c"/>切换成读模式之后,我们就可以读取缓冲区的数据了:// 创建一个limit()大小的字节数组(因为就只有limit这么多个数据可读)
        byte[] bytes = new byte[byteBuffer.limit()];


这样,当时select返回时,channel中已经可以read或者write了。总结选择器selector 将 通道channel感兴趣的IO事件注册监听,当其返回时,channel即可对这些IO事件进行处理,一般将这些读写操作都会放到单独的线程中执行,提高吞吐率

举报

相关推荐

0 条评论