Java NIO Selector实现原理

咱们先看看传统I/O模型工做模式:java

每条socket链路都由一个单独的线程负责处理,这对于大容量、高并发的应用程序来讲,使用上千万个线程来处理请求几乎是不可能实现的。git

多路复用IO模型的工做模式:github

在多路复用IO模型中,只须要使用一个线程就能够管理多个socket,并负责处理对应I/O事件。这对于构建高并发、大容量的服务端应用程序来讲是很是有意义。安全

从图中咱们能够看出,多路复用IO模型中,使用了一个Selector对象来管理多个通道,这是实现单个线程能够高效地处理多个socket上I/O事件的关键所在。下面开始介绍Java NIO中Selector所实现的功能和原理。并发

Selector简介

简单来讲,Selector就是SelectableChannel对象的多路复用器。一般调用Selector类的静态方法open来建立一个选择器对象,该方法使用系统默认SelectorProvider对象的openSelector方法来建立新的选择器。固然,还能够自定义实现SelectorProvider并重写openSelector方法来建立自定义选择器。socket

一个Channel对象注册到选择器以后,会返回一个SelectionKey对象,这个SelectionKey对象表明这个Channel和它注册的Selector间的关系。SelectionKey中维护着两个很重要的属性:interestOps、readyOps,并经过这两个属性管理通道上注册的事件。interestOps中保存了咱们但愿Selector监听Channel的哪些事件,在Selector每次作select操做时,若发现该Channel有咱们所监听的事件发生时,就会将感兴趣的监听事件设置到readyOps中,这样咱们能够根据事件的发生执行相应的I/O操做。ide

Selector的重要属性

每一个选择器中管理着三个SelectionKey集合:高并发

  • keys:该集合中保存了全部注册到当前选择器上的通道的SelectionKey对象;
  • selectedKeys:该集合中保存了上一次Selector选择期间,发生了就绪事件的通道的SelectionKey对象集合,它始终是keys的子集。
  • cancelledKeys:该集合保存了已经被取消但其关联的通道还未被注销的SelectionKey对象集合,它始终是keys的子集。

初始化Selector对象时,这三个集合都为空。当咱们调用Channel的register方法将通道注册到选择器时,一个SelectionKey对象会被加入到keys集合;调用通道的Close方法或直接调用选择器的cancel方法则会将一个SelectionKey对象添加到cancelledKeys集合,选择器下一次作选择操做时,将会清空cancelledKeys中保存的选择键,并从keys集合中删除;选择器作选择操做时,具备就绪事件的SelectionKey对象会被加入到selectedKeys集合中。编码

同时,每一个Selector中还维护了publicKeys和publicSelectedKeys两个视图,供客户端使用。publicKeys是keys的视图,调用Selector的keys()方法返回的就是publicKeys,publicKeys不支持添加和删除操做;publicSelectedKeys是selectedKeys的视图,它是是个不可增加的集合,即不支持add操做,但支持remove操做,调用publicSelectedKeys集合的remove操做实际是从selectedKeys中删除一个SelectionKey对象。咱们能够调用Selector的selectedKeys()方法访问publicSelectedKeys。spa

Selector建立

一般咱们使用Selector的静态工程方法open()来建立Selector对象:

public static Selector open() throws IOException {
    return SelectorProvider.provider().openSelector();
}

open方法负责向SPI发出请求,获取一个SelectorProvider实例。SelectorProvider的静态工厂方法 provider()决定由哪一个SelectorProvider对象来建立给定的Selector实例,一般是一个DefaultSelectorProvider实例。不一样操做系统对应着不一样的sun.nio.ch.DefaultSelectorProvider,Linux下DefaultSelectorProvider.create()会生成一个sun.nio.ch.EPollSelectorProvider类型的SelectorProvider,Windows环境下则生成sun.nio.ch.WindowsSelectorProvider类型的SelectorProvider。

当获取到SelectorProvider实例后,调用它的openSelector()便可建立一个特定的Selector对象。

Selection操做

Selector中提供了3种类型的selection操做:

select():该方法会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非当前线程发生中断或selector的wakeup方法被调用;

select(long timeout):该方法与select()相似,会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非下面3种状况任意一种发生:1 设置的超时时间到达;2 当前线程发生中断;3 selector的wakeup方法被调用;

selectNow():该方法不会发生阻塞,不管是否有channel发生就绪事件,都会当即返回。

选择器中最重要的就是selection操做,当咱们调用Selector的select方法时,selectedKeys集合会被更新,经过遍历selectedKeys,能够找到已经就绪的通道,从而处理各类I/O事件。select操做的大概过程以下:

  1. 检查cancelledKeys集合,若是它非空,从keys集合中移除全部存在于cancelledKeys集合中的SelectionKey对象,并将注销其通道,同时清空cancelledKeys;
  2. 向内核发起一个系统调用进行查询,以肯定选择器上注册的每一个通道所关心的事件是否就绪。若是没有通道已经准备好,线程可能会一直阻塞、阻塞指定时间,或当即返回,这主要依赖于特定select方法的调用;
  3. 系统调用返回,再次检查cancelledKeys集合;
  4. 系统调用返回后,对于那些没有就绪事件的通道将不会有任何的操做,对于那些已经有就绪事件的通道,将执行如下两种操做的一种: 
  • 若是通道的SelectionKey还未加入selectedKeys集合,将其添加到selectedKeys集合中,并修改ready集合,以便准确地标识该通道当前有哪些准备好的操做。先前记录在ready集合中的任何就绪信息都会被抛弃;
  • 不然,通道的SelectionKey已经存在于selectedKeys集合,修改ready集合,以便准确地标识该通道当前有哪些准备好的操做。全部以前记录在ready集合中已经再也不是就绪状态的操做不会被清除。事实上,全部的比特位都不会被清理。由操做系统决定的ready集合是与以前的ready集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的ready集合将是累积的。比特位只会被设置,不会被清理。

select操做返回的值是ready集合在步骤2中被修改的键的数量,而不是selectedKeys集合中的通道总数。返回值不是已准备好的通道的总数,而是从上一个select( )调用以后进入就绪状态的通道的数量。以前的调用中就绪的,而且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经再也不处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值多是0。

Selector唤醒

Selector中提供了使线程从被阻塞的select( )方法中优雅地退出的能力:

public abstract Selector wakeup();

若是一个线程在调用select()或select(long)方法时被阻塞,调用wakeup()会使线程当即从阻塞中唤醒;若是调用wakeup()期间没有select操做,下次调用select相关操做会当即返回。在Select期间,屡次调用wakeup()与调用一次效果是同样的。关于wakeup()的实现原理可参见文章Java NIO Selector的wakeup实现原理

Selector关闭

当咱们调用Selector的close()方法时,会首先执行wakeup操做,任何一个在选择操做中阻塞的线程都将被唤醒。同时会注销绑定在选择器上的全部通道,释放与此选择器相关联的任何其余资源。

若是选择已经处于关闭状态,再次调用close()方法不会由任何做用。若调用该选择器除close()和wakeup()以外的操做都会致使ClosedSelectorException异常。

SelectionKey

SelectionKey对象表明着一个Channel和它注册的Selector间的关系。其channel( )方法可返回与该键相关的SelectableChannel对象,而selector( )则返回相关的Selector对象。此外,SelectionKey中包含两个重要属性,两个以整数形式进行编码的比特掩码:

  • interestOps:表明对注册Channel所感兴趣的事件集合。interest集合是使用注册通道时给定的值初始化的,能够经过调用键对象的interestOps( int ops)方法修改。同时,能够调用键对象的interestOps()方法获取当前interest集合。当相关的Selector上的select( )操做正在进行时改变键的interest集合,不会影响那个正在进行的选择操做。全部更改将会在select( )的下一个调用中体现出来;
  • readyOps:表明interest集合中从上次调用select( )以来已经就绪的事件集合,它是interestOps的子集。注册通道时,初始化为0,只有在选择器选择操做期间可能被更新。能够调用键对象的readyOps()方法获取当前ready集合。需注意的是ready集合返回的就绪状态只是一个提示,不是保证。底层的通道在任什么时候候都会不断改变。其余线程可能在通道上执行操做并影响它的就绪状态。

SelectionKey中使用了四个常量来表明事件类型:

SelectionKey.OP_READ:通道已经准备好进行读取;

SelectionKey.OP_WRITE:通道已经准备好写入;

SelectionKey.OP_CONNECT:通道对应的socket已经准备好链接;

SelectionKey.OP_ACCEPT:通道对应的server socket已经准备好接受一个新链接。

注册通道时,若是咱们不止对一种操做感兴趣,能够用“位或”操做符将多个常量链接起来。以下:

socketChannel.register(selector, SelectionKey.OP_CONNECT|SelectionKey.OP_READ|SelectionKey.OP_WRITE);

在一次selection以后,咱们可使用如下几个方法来检测channel中什么事件已经就绪:

selectionKey.isAcceptable():是否已准备好接受新链接;
selectionKey.isConnectable():是否已准备好链接;
selectionKey.isReadable():是否已准备好读取;
selectionKey.isWritable():是否已准备好写入。

咱们也可使用相关的比特掩码来检测就绪状态,与调用上面的方法是一致的。如:

if ((selectionkey.readyOps( ) & SelectionKey.OP_READ) != 0) {
    readBuffer.clear( ); 
    key.channel( ).read (readBuffer); 
    ... 
}

一个selectionKey被建立后将保持有效,调用selectionKey的cancel()方法或关闭其通道或关闭其选择器将致使其失效。咱们能够调用isValid( )方法来检查selectionKey是否仍然有效。当咱们调用selectionKey的cancel()方法后,它将被放在相关的选择器的cancelledKeys集合中。注册关系不会当即被取消,可是selectionKey会当即失效。当再次调用select( )方法时(或者一个正在进行的select()调用结束时),cancelledKeys中的被取消的键将被清理掉。

selectionKey除了维护Channel和Selector的注册关系外,还提供了保存“附件”的功能,并提供方法访问它。这是一种容许咱们将任意对象与键关联的便捷方法。这个对象能够引用任何对象,例如业务对象、会话句柄、其余通道等等。当咱们在遍历与选择器相关的键时,可使用附加在selectionKey上的对象句柄来获取相关的上下文。attach( )方法将在selectionKey中保存所提供的对象的引用,attachment( )方法则用来获取与selectionKey关联的附件句柄。

关于SelectionKey还有最后一点须要注意,SelectionKey是线程安全的。修改interest集合的操做是经过Selector对象进行同步的,而选择器所使用的锁策略是依赖于具体实现的。所以若是Selector正在进行选择操做,则读取或写入interest集可能会阻塞不肯定的时间。

示例代码

selector=Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
    selector.select();
    Set<SelectionKey> selectionKeys=selector.selectedKeys();
    Iterator<SelectionKey> iterator=selectionKeys.iterator();
    while(iterator.hasNext()){
        SelectionKey selectionKey=iterator.next();
        if(selectionKey.isAcceptable()){
            // a connection was accepted by a ServerSocketChannel.
        }else if(selectionKey.isReadable()){
            // a channel is ready for reading
        }else if(selectionKey.isWritable()){
           // a channel is ready for writing
        }
        iterator.remove();
    }
}

完整的示例代码可移步https://github.com/JeffreyHy/jeffery-nio-study/tree/master/nio-demo

欢迎指出本文有误的地方,转载请注明原文出处https://my.oschina.net/7001/blog/1556102