选择器(Selector) 是 Java NIO 中用于检查一个或多个NIO Channel 状态是否处于可读、可写的组件。如此可以实现单线程管理多个 channel,从而可以管理多个网络链接。

阅读文章过程中有任何疑问请加入七日书摘微信群:
阅读文章的过程中如果有任何疑问,欢迎添加笔者为好友,拉您进【七日书摘】微信交流群,一起交流技术,一起打造高质量的职场技术交流圈子,抱团取暖,共同进步。
七日书摘官方群.jpg

为什么使用 Selector(Why Use a Selector)?

使用单线程处理多个 channels 的好处是只需要更少的线程来处理 channels。实际上,甚至可以只用一个线程来处理所有的 channels。从操作系统的角度来看,线程切换开销是比较大的,而且每个线程都要占用一些系统资源(比如内存),因此使用的线程越少越好。

不过也需要注意的是,现代操作系统和 CPU 在多任务处理方面越来越好,因此随着时间的推移多线程的开销影响也越来越小。如果一个 CPU 是多核的,那么不进行多任务处理反而是浪费了 CPU 的性能,不过这些设计讨论是另外的话题了。这里只需要说明,通过 Selector 我们可以实现单线程操作多个 channel。

下面是使用选择器处理 3 个通道的线程示例::
overview-selectors.png
Java NIO: A Thread uses a Selector to handle 3 Channel's

创建 Selector(Creating a Selector)

你可以调用 Selector.open() 方法创建一个 Selector,如下代码:

Selector selector = Selector.open();

注册 Channel 到 Selector 上(Registering Channels with the Selector)

为了将 Channel 和 Selector 配合使用,必须将 Channel 注册到 Selector上。使用 SelectableChannel.register() 方法来实现,代码如下:

channel.configureBlocking(false);//设置为非阻塞

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

与 Selector 一起使用时, Channel 必须是非阻塞模式的。所以不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式。而 Socket channel 可以正常使用。

注意 register() 方法的第二个参数,这个参数是一个“interest 集合”,意思时在通过 Selector 监听 Channel 时对那种事件感兴趣。有四种不同类型的事件可供监听:

  1. Connect
  2. Accept
  3. Read
  4. Write

channel 触发了一个事件意思是该事件处于就绪状态。因此当某个 channel 与 server 连接成功后那么就是“连接就绪”状态。server socket channel 接收请求连接时处于“可连接就绪”状态。channel 有数据可读时处于“读就绪”状态,channel 可以进行数据写入时处于“写就绪”状态。

上述的四种就绪状态用 SelectionKey 中的常量表示如下:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

如果对多个事件感兴趣,那么可以利用 “位或” 操作符结合多个常量,比如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

SelectionKey

在上一节中,当向 Selector 注册 Channel 时, register() 方法会返回一个 SelectionKey 对象,这个返回的对象包含了一些比较有趣的属性:

  1. The interest set
  2. The ready set
  3. The Channel
  4. The Selector
  5. An attached object (optional)

下面逐一介绍这 5 个属性。

Interest Set

这个 “Interest集合” 实际上就是我们希望处理的事件的集合,它的值就是注册时传入的参数,我们可以用 位与 运算把每个事件取出来,像下面这样:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;   

如您所见,您可以使用给定的 SelectionKey 常量和 interest 集来确定某个事件是否在 interest 中。

Ready Set

"Ready 集合" 中的值是当前 channel 处于就绪的值,一般来说在调用了 select 方法后都会需要用到 Ready 状态,在后续的章节中将介绍 select 。你可以按照如下方式操作Ready 集合:

int readySet = selectionKey.readyOps();

从 “Ready 集合” 中取值的操作类似月 “Interest 集合” 的操作,当然还有更简单的方法,SelectionKey 提供了一系列返回值为 boolean 的的方法:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel + Selector

从 SelectionKey 操作 Channel 和 Selector 非常简单,操作方式如下:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();    

Attaching Objects

你可以给一个 SelectionKey 附加一个 Object,这样做一方面可以方便你识别某个特定的 channel,同时也增加了 channel 相关的附加信息。例如,可以把用于 channel 的 buffer 附加到 SelectionKey 上。示例如下:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

你还可以在 register() 中向选择器(Selector)注册通道(Channel)时 附加对象的操作。像如下的操作:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

从 Selector 中选择 Channel(Selecting Channels via a Selector)

当向 Selector 注册了一个或多个 channel 后,就可以调用 select() 重载方法来获取 channel。select() 方法会返回所有处于就绪状态的 channel。 select() 方法具体如下:

  1. int select()
  2. int select(long timeout)
  3. int selectNow()

select() 方法在返回 Channel 之前处于阻塞状态。

select(long timeout) 和 select()一样,不过他的阻塞有一个超时值 timeout 限制(参数)。

selectNow() 不会阻塞,根据当前状态立刻返回合适的 channel。

select() 方法的返回一个 int 值表示有多少 channel 就绪。也就是自上一次 select() 方法调用后有多少 channel 变成就绪状态。举例来说,假设第一次调用select() 时正好有一个 channel 就绪,那么返回值是1,并且对这个 channel 做任何处理,接着再次调用 select(),此时恰好又有一个新的 channel 就绪,那么返回值还是 1。如果对第一个就绪的 channel 没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

selectedKeys()

一旦调用了 select() 方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用 selector 的selectedKeys() 方法访问“已选择键集(selected key set)”中的就绪通道。如下所示:

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

当向 Selector 注册 Channel 时,Channel.register() 方法会返回一个 SelectionKey 对象。这个对象代表了注册到该 Selector 的通道。可以通过 SelectionKey 的 selectedKeySet() 方法访问这些对象。

遍历这些 SelectionKey 可以通过如下方法。如下所示:

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

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

上述循环会迭代 key 集合,针对每个 key 我们单独判断他是处于何种就绪状态。

注意每次迭代末尾的 keyIterater.remove() 方法调用,Selector 本身并不会从选择键集中移除 SelectionKey 对象,必须在处理完通道(Channel)时自己移出。当下次 channel 变成就绪时,Selector 会再次将其放入已选择键集中。

SelectionKey.channel() 返回的 channel 实例需要强转为实际使用的具体 channel 类型,例如ServerSocketChannel 或 SocketChannel。

wakeUp()

由于调用 select() 而被阻塞的线程,可以通过调用Selector.wakeup() 来唤醒即便此时已然没有 channel 处于就绪状态。具体操作是,在另外一个线程调用 wakeup(),被阻塞与 select() 方法的线程就会立刻返回。
如果有其它线程调用了 wakeup() 方法,但当前没有线程阻塞在 select() 方法上,下个调用 select() 方法的线程会立即“醒来(wake up)”。

close()

当操作 Selector 完毕后,需要调用 close() 方法。close() 的调用会关闭 Selector 并使相关的SelectionKey 都无效。但 channel 本身不会被关闭。

完整的 Selector 案例(Full Selector Example)

这有一个完整的示例,首先打开一个 Selector,然后注册一个 channel 到这个 Selector 上,然后持续监控这个 Selector 的(接受、连接、读、写)状态是否就绪:

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


while(true) {

  int readyChannels = selector.selectNow();

  if(readyChannels == 0) continue;


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

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

英文原文链接:http://tutorials.jenkov.com/java-nio/selectors.html

------完------

推荐阅读:

Java NIO 简明教程 之 Java NIO 概述

Java NIO 简明教程 之 Java NIO Channel

Java NIO 简明教程 之 Java NIO 缓冲(Buffer)

Java NIO 简明教程 之 Java NIO Scatter/Gather

Java NIO 简明教程 之 Java NIO 通道之间的数据传输(Channel to Channel Transfers)")

Java基础知识面试题篇(2020年2月最新版)

更技术学习请进入七日书摘官方群: 七日书摘官方群

七日书摘官方群群聊二维码.png

参考资源:
https://blog.csdn.net/Andrew_Yuan/article/details/80215164
https://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-selector.html