Java NIO之Selector、SelectableChannel和SelectorProvider

高性能IO模型浅析 http://www.cnblogs.com/fanzhidongyzby/p/4098546.html
系统间通讯方式之(Java NIO多路复用模式)(四) http://blog.csdn.net/u010963948/article/details/78507255
java NIO selector全面深入理解 http://blog.csdn.net/lw305080/article/details/51205545
 Java NIO之Selector、SelectableChannel和SelectorProvider  http://blog.csdn.net/zilong_zilong/article/details/79588449

 

1.Selector 

        Selector是一个多路复用器,它负责管理被注册到其上的SelectableChannel。Selector的实现根据操作系统的不同而不同,目前多路复用IO实现主要包括四种:select、poll、epoll、kqueue,这里借用http://blog.csdn.net/u010963948/article/details/78507255博客中4种多路复用I/O实现的总结,列表如下:

IO模型 相对性能 关键思路 操作系统 JAVA支持情况
select 较高 Reactor windows/Linux

支持,Reactor模式(反应器设计模式)。

Linux操作系统的 kernels 2.4内核版本之前,默认使用select;

而目前windows下对同步IO的支持,都是select模型

poll 较高 Reactor Linux

Linux下的JAVA NIO框架,

Linux kernels 2.6内核版本之前使用poll进行支持。

也是使用的Reactor模式

epoll Reactor/Proactor Linux

Linux kernels 2.6内核版本及以后使用epoll进行支持;

Linux kernels 2.6内核版本之前使用poll进行支持;

另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO

kqueue Proactor Linux 目前JAVA的版本不支持

 

    1.1 Selector在Java里的默认实现

        Selector在Java里的默认实现如下图:

        默认JDK提供了对于POLL、KQUEUE这2个多路复用I/O模型的实现,对于其他实现则转交代理类SelectorProvider来提供,而SelectorProvider则根据不同操作系统的内核不同而不同,JDK对POLL、KQUEUE的实现类的组成绘图如下:

 

    1.2 Selector的创建、关闭

        Selector有2种创建方式

  • Selector.open():会调用操作系统底层的Selector实现来创建Selector;
  • SelectorProvider.openSelector():这种方式直接调用SelectorProvider类来获取操作系统底层的Selector来创建Selector。

        selector的关闭方法如下

  • Selector.close()

    1.3 Selector里维护的集合

        每个SelectableChannel会调用其SelectionKey register(Selector sel, int ops, Object att)方法向Selector注册,注册完毕后收到一个SelectionKey对象。Selector对象里维护着3个类型为java.util.Set的集合:

分类 描述
key集合 这个集合里都是注册到Selector的SelectableChannel对应的SelectionKey。要获得这些SelectionKey可以调用Selector.keys()。
selected-key集合 Selector会定期向操作系统内核询问是否有事件要通知到SelectableChannel,会将那些对这些事件感兴趣的SelectableChannel当时注册到Selector的SelectionKey复制一份到此集合,要获得这些SelectionKey集合可以调用Selector.selectedKeys(),这个集合是key集合的子集。
cancelled-key集合 这个集合里记录的是那些取消了对操作系统内核事件关注但并未取消对于Selector注册的SelectableChannel对应的SelectionKey的集合,这个集合不能直接访问,通常这个集合是key集合的子集。


 

        在SelectableChannel.register(Selector sel, int ops, Object att)调用后,会收到一个SelectionKey,并且SelectionKey已经在此方法调用后被添加到key集合,而cancelled-key集合中的SelectionKey会在Selector每次询问操作系统内核是否有事件通知时,从key集合中删除这些SelectionKey,而key集合自身不能直接被修改。

        在SelectionKey.cancel()或者SelectionKey对应的SelectableChannel.close()调用后,这个SelectionKey会被添加到cancelled-key集合,在下一个询问操作系统内核是否有事件通知时,会将这些取消了的SelectionKey从Selector对应的3个集合(key集合、selected-key集合、cancelled-key集合)中清理掉,同时这些SelectionKey对应的SelectableChannel也会从Selector中被注销。

        只有Selector.select()、Selector.select(long timeout)、Selector.selectNow()被调用并且操作系统内核是有事件通知才会将那些对这些事件感兴趣的SelectableChannel对应的SelectionKey放入selected-key集合,除此之外别无其它方法。只有直接调用selected-key集合的 Set.remove(Object o)或者通过Set.iterator()获取到的Iterator上调用Iterator.remove()来从selected-key集合删除某个SelectionKey,除此之外别其它方法。

 

    1.4 Selector的Selection操作

        Selector的Selection操作执行的3个步骤如下:

        (1)cancelled-key集合中的SelectionKey,首先会从Selector的key集合selected-key集合中被清除,接着cancelled-key集合中的SelectionKey对应的SelectableChannel会从Selector上进行注销,最后cancelled-key集合会被清空;

        (2)操作系统内核会被询问,从而获知哪些SelectionKey对应的SelectableChannel有感兴趣的事件通知发生,如果有SelectableChannel对此事件感兴趣,会执行如下2个步骤:

    • 如果该SelectableChannel对应的SelectionKey不在selected-key集合中,那么这个SelectableChannel对应的SelectionKey会被添加到selected-key集合中,同时该SelectableChannel对应的SelectionKey的成员变量readyOps会修改为本次事件对应的事件的值,而之前readyOps上记录的值会被覆盖;
    • 如果该SelectableChannel对应的SelectionKey在selected-key集合中,那么该SelectableChannel对应的SelectionKey的成员变量readyOps会修改为本次事件对应的事件的值,并且之前readyOps上记录的值会被保留,保留方式是将2个int进行或运算;

        如果key集合中的所有SelectionKey对应的SelectableChannel都对本次操作系统内核事件通知不感兴趣,那么Selector上的selected-key集合不会进行更新,并且SelectionKey的成员变量readyOps就不会被更新。

        (3)如果在步骤(2)中有SelectionKey被添加到selected-key集合,那么久按照步骤(1)的方式再次被处理。

 

    1.5 Selector的Concurrency控制

        Selector是线程安全的,可以被多个线程使用,但是Selector里的key集合、selected-key集合、cancelled-key集合不是线程安全的。

        Selector的Selector.select()、Selector.select(long timeout)、Selector.selectNow()操作按照Selector对象加锁、Selector里的key集合加锁、Selector里的selected-key集合加锁的先后顺序进行加锁来保证并发线程操作Selector的线程安全。同时也会在上面提到的步骤1和步骤3中对Selector里的cancelled-key集合进行加锁来保证并发线程操作Selector的线程安全。

        Selector的Selector.select()、Selector.select(long timeout)、Selector.selectNow()操作在被加锁处理期间,如果外界有新的SelectableChannel注册到Selector或者已有的SelectableChannel对应的SelectionKey上I/O事件发生变化,会在下一次Selector的Selector.select()、Selector.select(long timeout)、Selector.selectNow()操作执行时可见。

        SelectionKey.cancel()或者SelectableChannel.close()可能在任何时候调用,Selector不能保证其维护的的集合(key集合、selected-key集合、cancelled-key集合)中的SelectionKey是否有效和SelectionKey对应的SelectableChannel仍处于打开状态,因此应用程序要自己时刻使用同步机制来检查是否其它线程调用了SelectionKey.cancel()或者SelectableChannel.close()。

        线程在调用Selector的Selector.select()、Selector.select(long timeout)、Selector.selectNow()如果被阻塞,有如下3种方法中断:

    • Selector.wakeup():
    • Selector.close():
    • 阻塞线程的Thread.interrupt():

        Selector的key集合selected-key集合在多线程中使用是非线程安全的。如果想对这2个集合中的数据进行修改必须对集合进行加锁来保证同步。Selector的key集合selected-key集合的Set.iterator()获取到的Iterator容易fail-fast(fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件),如果尝试调用Iterator.remove()会获得一个java.util.ConcurrentModificationException异常。 

 

 

2.SelectableChannel

        SelectableChannel是一个连接到操作系统的连接通道,用于应用程序和操作系统交互、传递数据。连接到操作系统上的每个SelectableChannel会有一个专属的文件状态描述符标识。SelectableChannel是双向的,可以读取数据,也可以写数据。

        JDK对于SelectableChannel的默认实现如下:

        SelectableChannel要和I/O多路复用选择器Selector一起使用,首先需要调用SelectableChannel的SelectionKey register(Selector sel, int ops, Object att)方法来向Selector注册,这个方法会返回一个SelectionKey对象,SelectionKey维护着与之关联的Selector和SelectableChannel的引用。用一张图来看下SelectionKey组成:

        SelectableChannel可以是阻塞的也可以是非阻塞的,阻塞方式下,在操作系统内核I/O事件触发时必须等SelectableChannel处理完上一个I/O事件后才能进行本次I/O事件处理。非阻塞方式下,操作系统内核I/O事件触发被SelectableChannel处理不会被阻塞,相对来说其传输的字节数也会很少。

        默认创建的SelectableChannel都是阻塞的,非阻塞模式更适合于在多路复用I/O下使用。非阻塞SelectableChannel必须在注册到Selector之前被设置为非阻塞,一旦注册完毕后,只能在取消注册到Selector后才能修改非阻塞为阻塞。

 

3.SelectorProvider

        不同操作系统的I/O多路复用选择器各自内核实现不同,目前有select、poll、epoll、kqueue四种实现,JDK里与之对应的实现也随着操作系统的不同而不同,所以ORACLE官网提供的JDK下载需要区分不同的版本,因为每个操作系统底层的实现各不相同。

         JDK里对于Selector的实现都交由SelectorProvider的方法publicstatic SelectorProvider provider()来提供,这个方法的代码注释如下:

public static SelectorProvider provider() {
        synchronized (lock) {
            //provider不为空,直接返回provider
            if (provider != null) return provider;
            //AccessController.doPrivileged属于特权操作,意思是不管此方法由哪个用户发起,都无需对此操作涉及的资源(文件读写特权等等)进行检查
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            //由JDK的参数-Djava.nio.channels.spi.SelectorProvider=class设置的class来反射构造SelectorProvider
                            if (loadProviderFromProperty())
                                return provider;

                            //从jar中的目录META-INF/services配置文件中找参数java.nio.channels.spi.SelectorProvider=class设置的第一个class来反射构造SelectorProvider
                            if (loadProviderAsService())
                                return provider;

                            //调用不同操作系统版本的JDK里自带的sun.nio.ch.DefaultSelectorProvider来创建SelectorProvider
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }
        上面我们看到SelectorProvider的创建分三步进行:

        (1)由JDK的参数-Djava.nio.channels.spi.SelectorProvider=class设置的class来反射构造SelectorProvider,找不到就跳转到步骤(2)

        (2)从jar中的目录META-INF/services配置文件中找参数java.nio.channels.spi.SelectorProvider=class设置的第一个class来反射构造SelectorProvider,找不到就跳转到步骤(3)       

        (3)调用不同操作系统版本的JDK里自带的sun.nio.ch.DefaultSelectorProvider来创建SelectorProvider

        一般都会走到最后的步骤(3),而这个步骤里创建的SelectorProvider在各个操作系统对应的JDK里各不相同。sun.nio.ch.DefaultSelectorProvider这个类最终编译后放置在JDK的安装根目录下的jre/lib/rt.jar里。

        windows版本JDK里sun.nio.ch.DefaultSelectorProvider源代码如下:

package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
public class DefaultSelectorProvider
{
  public static SelectorProvider create()
  {
    return new WindowsSelectorProvider();
  }
}

        MAC版本JDK里sun.nio.ch.DefaultSelectorProvider源代码如下:

package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
public class DefaultSelectorProvider
{
  public static SelectorProvider create()
  {
    return new KQueueSelectorProvider();
  }
}

        Linux版本JDK里sun.nio.ch.DefaultSelectorProvider源代码如下:

package sun.nio.ch;
import java.nio.channels.spi.SelectorProvider;
import java.security.AccessController;
import sun.security.action.GetPropertyAction;
public class DefaultSelectorProvider
{
  public static SelectorProvider create()
  {
    String str1 = (String)AccessController.doPrivileged(new GetPropertyAction("os.name"));

    if ("SunOS".equals(str1)) {
      return new DevPollSelectorProvider();
    }

    if ("Linux".equals(str1)) {
      String str2 = (String)AccessController.doPrivileged(new GetPropertyAction("os.version"));
      String[] arrayOfString = str2.split("\\.", 0);
      if (arrayOfString.length >= 2) {
        try {
          int i = Integer.parseInt(arrayOfString[0]);
          int j = Integer.parseInt(arrayOfString[1]);
          if ((i > 2) || ((i == 2) && (j >= 6))) {
            return new EPollSelectorProvider();
          }
        }
        catch (NumberFormatException localNumberFormatException)
        {
        }
      }
    }
    return new PollSelectorProvider();
  }
}

        不同操作系统里最终SelectorProvider的实现类如下:

操作系统下JDK JDK里SelectorProvider实现
windows JDK sun.nio.cn.WindowsSelectorProvider
MAC JDK sun.nio.ch.KQueueSelectorProvider
Linux JDK

1)os.name=SunOS时,sun.nio.ch.DevPollSelectorProvider

 

2)os.name=Linux并且os.version版本号中第一个版本大于2或者

第一个版本号等于2且第二个版本大于6时,

sun.nio.chEPollSelectorProvider

 

Linux kernels 2.6内核版本及以后使用epoll进行支持;

Linux kernels 2.6内核版本之前使用poll进行支持;

比如Linux version 3.10.0-327.el7.x86_64就是用的

sun.nio.chEPollSelectorProvider