http://zachary-guo.iteye.com/blog/1565856
在详细了解 Selector API 之前,你需要知道一点和 Selector 内部工作原理相关的知识。就像在 NIO - 使用选择键 中]探讨的那样,选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在 SelectionKey 对象中的。每一个 Selector 对象维护三个键的集合:
- public abstract class Selectory {
- // This is a partial API listing
- public abstract Set keys();
- public abstract Set selectedKeys();
- public abstract int select() throws IOException;
- public abstract int select(long timeout) throws IOException;
- public abstract int selectNow() throws IOException;
- public abstract void wakeup();
- }
- 已注册的键的集合(Registered key set) 与选择器关联的已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引 java.lang.UnsupportedOperationException。
- 已选择的键的集合(Selected key set) 已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的 interest 集合中的操作。这个集合通过 selectedKeys() 方法返回(并有可能是空的)。
不要将已选择的键的集合与 ready 集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的 ready 集合,指示了所关联的通道已经准备好的操作。
键可以直接从这个集合(已选择的键的集合)中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出 java.lang.UnsupportedOperationException。
- 已取消的键的集合(Cancelled key set) 已注册的键的集合的子集,这个集合包含了 cancel() 方法(SelectionKey 类的方法)被调用过的键(这个键已经被无效化),但它们还没有被注销 。这个集合是选择器对象的私有成员,因而无法直接访问。
◇ 选择过程
Selector 类的核心是选择过程。基本上来说,选择器是对 select()、poll() 等本地调用(native call)或者类似的操作系统特定的系统调用的一个包装。但是 Selector 所作的不仅仅是简单地向本地代码传送参数。它对每个选择操作应用了特定的过程。对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。
Selector 类的 select() 方法有以下三种不同的形式:
- int select() 这种调用在没有通道就绪时将无限阻塞。一旦至少有一个已注册的通道就绪,选择器的选择键就会被更新,并且每个就绪的通道的 ready 集合也将被更新。返回值将会是已经确定就绪的通道的数目。正常情况下,这些方法将返回一个非零的值,因为直到一个通道就绪前它都会阻塞。
- int select(long timeout) 这种调用与之前的例子完全相同,除了如果在你提供的超时时间(以毫秒计算)内没有通道就绪时,它将返回 0。如果一个或者多个通道在时间限制终止前就绪,键的状态将会被更新,并且方法会在那时立即返回。将超时参数指定为 0 表示将无限期等待,那么它就在各个方面都等同于使用无参数版本的 select() 了。
- int selectNow() 这种种形式是完全非阻塞的。selectNow() 方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回 0。
1. 已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。
2. 已注册的键的集合中的键的 interest 集合将被检查。在这个步骤中的检查执行过后,对 interest 集合的改动不会影响剩余的检查过程。
一旦就绪条件被定下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态。依赖于特定的 select() 方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。对于那些还没准备好的通道将不会执行任何的操作。对于那些操作系统指示至少已经准备好 interest 集合中的一种操作的通道,将执行以下两种操作中的一种:
- 如果通道的键还没有处于已选择的键的集合中,那么键的 ready 集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。
- 否则,也就是键在已选择的键的集合中。键的 ready 集合将被表示操作系统发现的当前已经准备好的操作的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的 ready 集合是与之前的 ready 集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的 ready 集合将是累积的。比特位只会被设置,不会被清理。假设之前的 ready 集合为 100,此次 010 的操作已就绪,此时的 ready 集合为 110,而不是 010。这就是累积,不会被清理。
4. select 操作返回的值是 ready 集合在步骤 2 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个 select() 调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是 0。
◇ 停止选择过程
Selector 的 API 中的最后一个方法, wakeup(),提供了使线程从被阻塞的 select() 方法中优雅地退出的能力:
- public abstract class Selector {
- // This is a partial API listing
- public abstract void wakeup();
- }
有三种方式可以唤醒在 select() 方法中睡眠的线程:
- 调用 wakeup() 调用 Selector 对象的 wakeup() 方法将使得选择器上的第一个还没有返回的选择操作立即返回。
- 调用 close() 如果选择器的 close() 方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,就像 wakeup() 方法被调用了一样。与选择器相关的通道将被注销,而键将被取消。
- 调用 interrupt() 如果睡眠中的线程的 interrupt() 方法被调用,它的返回状态将被设置。如果被唤醒的线程之后将试图在通道上执行 I/O 操作,通道将立即关闭,然后线程将捕捉到一个异常。使用 wakeup() 方法将会优雅地将一个在 select() 方法中睡眠的线程唤醒。如果你想让一个睡眠的线程在直接中断之后继续执行,需要执行一些步骤来清理中断状态(参见 Thread.interrupted() 的相关文档)。
Selector 对象将捕捉 InterruptedException 异常并调用 wakeup() 方法。请注意这些方法中的任意一个都不会关闭任何一个相关的通道。中断一个选择器与中断一个通道是不一样的。选择器不会改变任意一个相关的通道,它只会检查它们的状态。当一个在 select() 方法中睡眠的线程中断时,对于通道的状态而言,是不会产生歧义的。
◇ 管理选择键
选择是累积的。一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个键。并且,一旦一个键处于已选择的键的集合中,这个键的 ready 集合将只会被设置,而不会被清理。乍一看,这好像会引起麻烦,因为选择操作可能无法表现出已注册的通道的正确状态。它提供了极大的灵活性,但把合理地管理键以确保它们表示的状态信息不会变得陈旧的任务交给了程序员。
合理地使用选择器的秘诀是理解选择器维护的选择键集合所扮演的角色(参见 选择过程 小节,特别是选择过程的第二步)。最重要的部分是当键已经不再在已选择的键的集合中时将会发生什么。当通道上的至少一个感兴趣的操作就绪时,键的 ready 集合就会被清空,并且当前已经就绪的操作将会被添加到 ready 集合中。该键之后将被添加到已选择的键的集合中。
清理一个 SelectKey 的 ready 集合的方式是将这个键从已选择的键的集合中移除。选择键的就绪状态只有在选择器对象在选择操作过程中才会修改。处理思想是只有在已选择的键的集合中的键才被认为是包含了合法的就绪信息的。这些信息将在键中长久地存在,直到键从已选择的键的集合中移除,以通知选择器你已经看到并对它进行了处理。如果下一次通道的一些感兴趣的操作发生时,键将被重新设置以反映当时通道的状态并再次被添加到已选择的键的集合中。
这种框架提供了很多灵活性。通常的做法是在选择器上调用一次 select 操作(这将更新已选择的键的集合),然后遍历 selectKeys() 方法返回的键的集合。在按顺序进行检查每个键的过程中,相关的通道也根据键的就绪集合进行处理。然后键将从已选择的键的集合中被移除(通过在 Iterator 对象上调用 remove() 方法),然后检查下一个键。完成后,通过再次调用 select() 方法重复这个循环。