昨天我们介绍了一下SelectorProvider和IO multiplexing.特别是IO multiplexing中的epoll系统调用,是Linux版本的Java的NIO的核心实现.
那今天我们就来介绍一下, Java NIO中的核心组件, Selector和Channel.这两个组件,对于熟悉Java OIO,而不熟悉Java NIO的朋友来说,理解其作用是极其不易的.
前提条件
在阅读这篇文章之前,如果各位不熟悉甚至没有听说过IO multiplexing中的epoll,请务必花时间去了解一下.了解了它们之后,就很容易理解Java NIO的实现了.
这里我们讲解的是Linux版本且内核版本大于2.6的Java NIO的实现,对于其他的系统或者内核版本较低的Java NIO,其具体实现是不一样的.
举例来说, Linux 内核版本大于等于2.6的Java NIO是采用epoll来实现的.而Linux内核版本小于2.6的Java NIO,则是采用poll来实现的.
Selector和Channel
Selector和Channel的关系,如下图所示:
各位如果了解过epoll的话,应该知道epoll_create操作会创建一个需要被监听的file descriptor.然后,epoll_ctl操作会为告诉内核,需要监听一个file descripitor的什么事件.最后,使用epoll_wait来告诉内核开始监听.
这里我们就可以把Selector比作epoll中的内核.把Channel比作epoll_create操作创建的file descriptor.这样就很容易理解了吧.
因为Java NIO实际上是给我们对IO multiplexing进行了封装,隐藏了其底层的实现.所以我们完全可以这样来理解.
贴出在Java NIO tutorial中看到的一个图片,
对于这张图片,我实在是不能苟同其说法.我们可以看到,在这张图片中,我们可以看到,一个线程中只有一个Selector,每个Selector负责监控三个Channel.而实际上,一个线程中,并不是必须只能有一个Selector.一个Selector也不是只能注册三个Channel.
在AbstractSelector的源码中,我们可以看到,实际上它只维护了一个不再监听的Channel的集合:
我们查看具体的Selector的父类,SelectorImpl,中的register方法的实现.跟具体的Selector实现相关的类,在JDK提供的src.zip源码包中是找不到的.这里使用CFR反编译器反编译rt.jar包.从中找到其实现.
我们可以看到,它会把Channel进一步封装成SelectionKeyImpl.然后使用implRegister方法来实现具体的注册过程.从SelectorImpl的源码中,我们同样可以看到,implRegister方法是一个抽象方法,需要其子类来实现具体的注册过程.
这里我们感兴趣的子类是EPollSelectorImpl,我们查看其源码,可以看到其中维护了一个从file descriptor到SelectionKeyImpl的Map.我们刚刚也提到了,SelectionKeyImpl中,包装了一个Channel,
我们从EPollSelectorImpl的implRegister方法中,也没有看到会对Map<Integer, SelectionKeyImpl>这个表示EPollSelectorImpl维护的Channel的Map进行尺寸限制的操作.即并没有限制一个EPollSelectorImpl可以注册的Channel的数量.
反而是在Channel中,维护了它向Selector注册时,Selector给其返回的SelectionKey的集合.相当于维护了它已经注册的Selector的集合.
我们查看AbstractSelectableChannel的向Selector注册的源码:
我们可以看到,它会把Selector给它返回的SelectionKey加入到上面我们说过的那个集合中,我们看看addKey()方法的具体实现:
在这里我们就可以看到,默认情况下,Channel会创建一个容量为3的表示它注册的Selector的集合.当它需要向更多的Selector注册时,则对这个集合进行扩容.
而并没有提到一个Selector中最多可以注册多少个Channel.