这里,主要是摘抄自《Java NIO》。
从最基础的层面来看,选择器提供了询问通道是否已经准备好执行每个I/0操作的能力。例如,我们需要了解一个SocketChannel对象是否还有更多的字节需要读取,或者我们需要知道ServerSocketChannel是否有需要准备接受的连接。
在与SelectableChannel联合使用时,选择器提供了这种服务,但这里面有更多的事情需要去了解。就绪选择的真正价值在于潜在的大量的通道可以同时进行就绪状态的检查。调用者可以轻松地决定多个通道中的哪一个准备好要运行。有两种方式可以选择:被激发的线程可以处于休眠状态,直到一个或者多个注册到选择器的通道就绪,或者它也可以周期性地轮询选择器,看看从上次检查之后,是否有通道处于就绪状态。如果您考虑一下需要管理大量并发的连接的网络服务器(web server)的实现,就可以很容易地想到如何善加利用这些能力。
乍一看,好像只要非阻塞模式就可以模拟就绪检查功能,但实际上还不够。非阻塞模式同时还会执行您请求的任务,或指出它无法执行这项任务。这与检查它是否能够执行某种类型的操作是不同的。举个例子,如果您试图执行非阻塞操作,并且也执行成功了,您将不仅仅发现read( )是可以执行的,同时您也已经读入了一些数据。就下来您就需要处理这些数据了。
效率上的要求使得您不能将检查就绪的代码和处理数据的代码分离开来,至少这么做会很复杂。
即使简单地询问每个通道是否已经就绪的方法是可行的,在您的代码或一个类库的包里的某些代码需要遍历每一个候选的通道并按顺序进行检查的时候,仍然是有问题的。这会使得在检查每个通道是否就绪时都至少进行一次系统调用,这种代价是十分昂贵的,但是主要的问题是,这种检查不是原子性的。列表中的一个通道都有可能在它被检查之后就绪,但直到下一次轮询为止,您并不会觉察到这种情况。最糟糕的是,您除了不断地遍历列表之外将别无选择。您无法在某个您感兴趣的通道就绪时得到通知。
这就是为什么传统的监控多个socket的Java解决方案是为每个socket创建一个线程并使得线程可以在read( )调用中阻塞,直到数据可用。这事实上将每个被阻塞的线程当作了socket监控器,并将Java虚拟机的线程调度当作了通知机制。这两者本来都不是为了这种目的而设计的。程序员和Java虚拟机都为管理所有这些线程的复杂性和性能损耗付出了代价,这在线程数量的增长失控时表现得更为突出。
真正的就绪选择必须由操作系统来做。操作系统的一项最重要的功能就是处理I/O请求并通知各个线程它们的数据已经准备好了。选择器类提供了这种抽象,使得Java代码能够以可移植的方式,请求底层的操作系统提供就绪选择服务。
在JDK 1.4中,有四种被定义的可选择操作:读(read),写(write),连接(connect)和接受(accept)。
并非所有的操作都在所有的可选择通道上被支持。例如,SocketChannel不支持accept。试图注册不支持的操作将导致IllegalArgumentException。您可以通过调用validOps( )方法来获取特定的通道所支持的操作集合。
- 已注册的键的集合(Registered key set)
与选择器关联的已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys( )方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引java.lang.UnsupportedOperationException。
- 已选择的键的集合(Selected key set)
已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过selectedKeys( )方法返回(并有可能是空的)。
不要将已选择的键的集合与ready集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。
键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。
- 已取消的键的集合(Cancelled key set)
已注册的键的集合的子集,这个集合包含了cancel( )方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。
在一个刚初始化的Selector对象中,这三个集合都是空的。
Selector类的核心是选择过程。这个名词您已经在之前看过多次了——现在应该解释一下了。基本上来说,选择器是对select( )、poll( )等本地调用(native call)或者类似的操作系统特定的系统调用的一个包装。但是Selector所作的不仅仅是简单地向本地代码传送参数。它对每个选择操作应用了特定的过程。对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。
选择操作是当三种形式的select( )中的任意一种被调用时,由选择器执行的。不管是哪一种形式的调用,下面步骤将被执行:
1.已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。
2.已注册的键的集合中的键的interest集合将被检查。在这个步骤中的检查执行过后,对interest集合的改动不会影响剩余的检查过程。
一旦就绪条件被定下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态。依赖于特定的select( )方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。
直到系统调用完成为止,这个过程可能会使得调用线程睡眠一段时间,然后当前每个通道的就绪状态将确定下来。对于那些还没准备好的通道将不会执行任何的操作。对于那些操作系统指示至少已经准备好interest集合中的一种操作的通道,将执行以下两种操作中的一种:
a.如果通道的键还没有处于已选择的键的集合中,那么键的ready集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。
b.否则,也就是键在已选择的键的集合中。键的ready集合将被表示操作系统发现的当前已经准备好的操作的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的ready集合是与之前的ready集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的ready集合将是累积的。比特位只会被设置,不会被清理。(正式因为这个原因,这个键在使用完毕后,需要从已选集合中移除,后面还会有介绍)
3.步骤2可能会花费很长时间,特别是所激发的线程处于休眠状态时。与该选择器相关的键可能会同时被取消。当步骤2结束时,步骤1将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注销。
4.select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个select( )调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是0。
调用Selector对象的wakeup( )方法将使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有在进行中的选择,那么下一次对select( )方法的一种形式的调用将立即返回。后续的选择操作将正常进行。在选择操作之间多次调用wakeup( )方法与调用它一次没有什么不同。
选择是累积的。一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个键。并且,一旦一个键处于已选择的键的集合中,这个键的ready集合将只会被设置,而不会被清理。乍一看,这好像会引起麻烦,因为选择操作可能无法表现出已注册的通道的正确状态。它提供了极大的灵活性,但把合理地管理键以确保它们表示的状态信息不会变得陈旧的任务交给了程序员。
合理地使用选择器的秘诀是理解选择器维护的选择键集合所扮演的角色。(参见4.3.1小节,特别是选择过程的第二步。)最重要的部分是当键已经不再在已选择的键的集合中时将会发生什么。当通道上的至少一个感兴趣的操作就绪时,键的ready集合就会被清空,并且当前已经就绪的操作将会被添加到ready集合中。该键之后将被添加到已选择的键的集合中。
清理一个SelectKey的ready集合的方式是将这个键从已选择的键的集合中移除。选择键的就绪状态只有在选择器对象在选择操作过程中才会修改。处理思想是只有在已选择的键的集合中的键才被认为是包含了合法的就绪信息的。这些信息将在键中长久地存在,直到键从已选择的键的集合中移除,以通知选择器您已经看到并对它进行了处理。如果下一次通道的一些感兴趣的操作发生时,键将被重新设置以反映当时通道的状态并再次被添加到已选择的键的集合中。
这种框架提供了很多灵活性。通常的做法是在选择器上调用一次select操作(这将更新已选择的键的集合),然后遍历selectKeys( )方法返回的键的集合。在按顺序进行检查每个键的过程中,相关的通道也根据键的就绪集合进行处理。然后键将从已选择的键的集合中被移除(通过在Iterator对象上调用remove( )方法),然后检查下一个键。完成后,通过再次调用select( )方法重复这个循环。例4-1中的代码是典型的服务器的例子。
// Allocate an unbound server socket channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// Get the associated ServerSocket to bind it with
ServerSocket serverSocket = serverChannel.socket();
// Create a new Selector for use below
Selector selector = Selector.open();
// Set the port the server channel will listen to
serverSocket.bind(new InetSocketAddress(port));
// Set nonblocking mode for the listening socket
serverChannel.configureBlocking(false);
// Register the ServerSocketChannel with the Selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// This may block for a long time. Upon returning, the selected set contains keys of the ready
channels. int n = selector.select();
if (n == 0) {
continue; // nothing to do
}
// Get an iterator over the set of selected keys
Iterator it = selector.selectedKeys().iterator();
// Look at each key in the selected set
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
// Is a new connection coming in?
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
registerChannel(selector, channel, SelectionKey.OP_READ);
sayHello(channel);
}
// Is there data to read on this channel?
if (key.isReadable()) {
readDataFromSocket(key);
}
// Remove key from selected set; it's been handled
it.remove();
}