详解 Java NIO

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 方法的调用链。

详解 Java NIO

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

详解 Java NIO

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 的文件读写是非阻塞的,明明就是阻塞的。

详解 Java NIO

创建一个选择器一般是通过 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 keys = selector.selectedKeys();

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

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

详解 Java NIO

服务端代码:

详解 Java NIO

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

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

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

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

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

最后:

给大家分享资深架构师录制的视频:(有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构)等这些成为架构师必备的内容,还有阿里大牛直播讲解技术!

详解 Java NIO

后台私信回复“架构” 就可以免费获得这些视频资料!


分享到:


相關文章: