继续----------------》》》》》》》》》》》
使用选择器----------->>>>>>>>>>>>
选择过程 :在详细了解 API 之前,您需要知道一点和 Selector 内部工作原理相关的知识。就像上面探讨的那样,选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey对象中的。每一个 Selector 对象维护三个键的集合:
public abstract class Selector
{
// 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( ) 方法被调用过的键(这个键已经被无效化),但它们还没有被注销 。这个集合是选择器对象的私有成员,因而无法直接访问。
在一个刚初始化的 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 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个selec t( ) 调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是 0。
使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化。注销通道是一个潜在的代价很高的操作,这可能需要重新分配资源(请记住,键是与通道相关的,并且可能与它们相关的通道对象之间有复杂的交互)。清理已取消的键,并在选择操作之前和之后立即注销通道,可以消除它们可能正好在选择的过程中执行的潜在棘手问题。这是另一个兼顾健壮性的折中方案。
Selector 类的select( )方法有以下三种不同的形式: 这三种select 的形式,仅仅在它们在所注册的通道当前都没有就绪时,是否阻塞的方面有所不同。最简单的没有参数的形式可以用如下方式调用: 这种调用在没有通道就绪时将无限阻塞。一旦至少有一个已注册的通道就绪,选择器的选择键就会被更新,并且每个就绪的通道的 ready 集合也将被更新。返回值将会是已经确定就绪的通道的数目。正常情况下,这些方法将返回一个非零的值,因为直到一个通道就绪前它都会阻塞。但是它也可以返回非 0 值,如果选择器的 wakeup( )方法被其他线程调用。
有时您会想要限制线程等待通道就绪的时间。这种情况下,可以使用一个接受一个超时参数的select( )方法的重载形式: 这种调用与之前的例子完全相同,除了如果在您提供的超时时间(以毫秒计算)内没有通道就绪时,它将返回0。如果一个或者多个通道在时间限制终止前就绪,键的状态将会被更新,并且方法会在那时立即返回。将超时参数指定为 0 表示将无限期等待,那么它就在各个方面都等同于使用无参数版本的 select( )了。
就绪选择的第三种也是最后一种形式是完全非阻塞的:
int n = selector.selectNow();
selectNow() 方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回 0。
停止选择过程:Selector的API 中的最后一个方法,wakeup( ),提供了使线程从被阻塞的 select( ) 方法中优雅地退出的能力:
public abstract class Selector
{
// This is a partial API listing
public abstract void wakeup( );
}
public abstract int select() throws IOException
-
Selects a set of keys whose corresponding channels are ready for I/O operations.
This method performs a blocking selection operation. It returns only after at least one channel is selected, this selector's
wakeup
method is invoked, or the current thread is interrupted, whichever comes first.
有三种方式可以唤醒在 select( )方法中睡眠的线程:(省略了详细内容)
调用wakeup( )
调用close( )
调用interrupt( )
管理选择键:既然我们已经理解了问题的各个部分是怎样结合在一起的,那么是时候看看它们在正常的使用中是如何交互的了。为了有效地利用选择器和键提供的信息,合理地管理键是非常重要的。
选择是累积的。一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个键。并且,一旦一个键处于已选择的键的集合中,这个键的 ready 集合将只会被设置,而不会被清理。乍一看,这好像会引起麻烦,因为选择操作可能无法表现出已注册的通道的正确状态。它提供了极大的灵活性,但把合理地管理键以确保它们表示的状态信息不会变得陈旧的任务交给了程序员。
合理地使用选择器的秘诀是理解选择器维护的选择键集合所扮演的角色。(特别是选择过程的第二步。)最重要的部分是当键已经不再在已选择的键的集合中时将会发生什么。当通道上的至少一个感兴趣的操作就绪时,键的 ready 集合就会被清空,并且当前已经就绪的操作将会被添加到 ready 集合中。该键之后将被添加到已选择的键的集合中。
清理一个 SelectKey 的ready 集合的方式是将这个键从已选择的键的集合中移除。选择键的就绪状态只有在选择器对象在选择操作过程中才会修改。处理思想是只有在已选择的键的集合中的键才被认为是包含了合法的就绪信息的。这些信息将在键中长久地存在,直到键从已选择的键的集合中移除,以通知选择器您已经看到并对它进行了处理。如果下一次通道的一些感兴趣的操作发生时,键将被重新设置以反映当时通道的状态并再次被添加到已选择的键的集合中。
这种框架提供了很多灵活性。通常的做法是在选择器上调用一次 select 操作(这将更新已选择的键的集合),然后遍历 selectKeys( )方法返回的键的集合。在按顺序进行检查每个键的过程中,相关的通道也根据键的就绪集合进行处理。然后键将从已选择的键的集合中被移除(通过在 Iterator对象上调用 remove()方法),然后检查下一个键。完成后,通过再次调用 select( ) 方法重复这个循环。下例中的代码是典型的服务器的例子。
使用select( )来为多个通道提供服务:
package com.ronsoft.books.nio.channels;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.nio.channels.SelectableChannel;
import java.net.ServerSocket;
import java.net.InetSocketAddress;
import java.util.Iterator;
/**
* Simple echo -back server which listens for incoming stream connections and
* echoes back whatever it reads. A single Selector object is used to listen to
* the server socket (to accept new connections) and all the active socket
* channels.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class SelectSockets {
public static int PORT_NUMBER = 1234;
public static void main(String[] argv) throws Exception {
new SelectSockets().go(argv);
}
public void go(String[] argv) throws Exception {
int port = PORT_NUMBER;
if (argv.length > 0) { // Override default listen por t
port = Integer.parseInt(argv[0]);
}
System.out.println("Listening on port " + port);
// 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();//This method performs a blocking selection operation.
if (n == 0) {
continue; // nothing to do
}
// Get an iterator over the set of selected keys
Iterator<SelectionKey> 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();
}
}
}
// ----------------------------------------------------------
/**
* Register the given channel with the given selector for the given
* operations of interes t
*/
protected void registerChannel(Selector selector,
SelectableChannel channel, int ops) throws Exception {
if (channel == null) {
return; // could happen
}
// Set the new channel nonblocking
channel.configureBlocking(false);
// Register it with the selector
channel.register(selector, ops);
}
// ----------------------------------------------------------
// Use the same byte buffer for all channels. A single thread is
// servicing all the channels, so no danger of concurrent acccess.
private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
/**
* Sample data handler method for a channel with data ready to read.
*
* @param key
* A SelectionKey object associated with a channel determined by
* the selector to be ready for reading. If the channel returns
*
* an EOF condition, it is closed here, which automatically
* invalidates the associated key. The selector will then de
* -register the channel on the next select call.
*/
protected void readDataFromSocket(SelectionKey key) throws Exception {
SocketChannel socketChannel = (SocketChannel) key.channel();
int count;
buffer.clear(); // Empty buffer
// Loop while data is available; channel is nonblocki ng
while ((count = socketChannel.read(buffer)) > 0) {
buffer.flip(); // Make buffer readable
// Send the data; don't assume it goes all at once
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// WARNING: the above loop is evil. Because
// it's writing back to the same nonblocking
// channel it read the data from, this code can
// potentially spin in a busy loop. In real life
// you'd do something more useful than this.
buffer.clear(); // Empty buffer
}
if (count < 0) {
// Close channel on EOF, invalidates the key
socketChannel.close();
}
}
// ----------------------------------------------------------
/**
* Spew a greeting to the incoming client connection.
*
* @param channel
* The newly connected SocketChannel to say hello to.
*/
private void sayHello(SocketChannel channel) throws Exception {
buffer.clear();
buffer.put("Hi there!\r\n".getBytes());
buffer.flip();
channel.write(buffer);
}
}
上例实现了一个简单的服务器。它创建了 ServerSocketChannel 和Selector 对象,并将通道注册到选择器上。我们不在注册的键中保存服务器socket 的引用,因为它永远不会被注销。这个无限循环在最上面先调用了select( ) ,这可能会无限期地阻塞。当选择结束时,就遍历选择键并检查已经就绪的通道。
如果一个键指示与它相关的通道已经准备好执行一个accecpt( )操作,我们就通过键获取关联的通道,并将它转换为SeverSocketChannel 对象。我们都知道这么做是安全的,因为只有ServerSocketChannel 支持OP_ACCEPT 操作。我们也知道我们的代码只把对一个单一的ServerSocketChannel 对象的OP_ACCEPT 操作进行了注册。通过对服务器socket 通道的引用,我们调用了它的 accept( ) 方法,来获取刚到达的 socket 的句柄。返回的对象的类型是SocketChannel ,也是一个可选择的通道类型。这时,与创建一个新线程来从新的连接中读取数据不同,我们只是简单地将 socket 通道注册到选择器上。我们通过传入 OP_READ 标记,告诉选择器我们关心新的 socket 通道什么时候可以准备好读取数据。
------------------疑问? 既然SelectionKey.OP_READ,那么为何sayHello中是channel.write(buffer),完全搞不明白啊????????????
如果键指示通道还没有准备好执行 accept( ) ,我们就检查它是否准备好执行 read( ) 。任何一个这么指示的socket 通道一定是之前ServerSocketChannel 创建SocketChannel 对象之一,并且被注册为只对读操作感兴趣。对于每个有数据需要读取的 socket 通道,我们调用一个公共的方法来读取并处理这个带有数据的 socket 。需要注意的是这个公共方法需要准备好以非阻塞的方式处理 socket 上的不完整的数据。它需要迅速地返回,以其他带有后续输入的通道能够及时地得到处理。上例中只是简单地对数据进行响应,将数据写回 socket ,传回给发送者。
在循环的底部,我们通过调用 Iterator (迭代器)对象的 remove() 方法,将键从已选择的键的集合中移除。键可以直接从 selectKeys() 返回的 Set 中移除,但同时需要用 Iterator 来检查集合,您需要使用迭代器的 remove() 方法来避免破坏迭代器内部的状态。
并发性:选择器对象是线程安全的,但它们包含的键集合不是。通过 keys( ) 和selectKeys( )返回的键的集合是 Selector 对象内部的私有的 Set 对象集合的直接引用。这些集合可能在任意时间被改变。已注册的键的集合是只读的。如果您试图修改它,那么您得到的奖品将是一个java.lang.UnsupportedOperationException,但是当您在观察它们的时候,它们可能发生了改变的话,您仍然会遇到麻烦。Iterator 对象是快速失败的(fail -fast) :如果底层的 Set 被改变了,它们将会抛java.util.ConcurrentModificationException,因此如果您期望在多个线程间共享选择器和/ 或键,请对此做好准备。您可以直接修改选择键,但请注意您这么做时可能会彻底破坏另一个线程的 Iterator 。
如果在多个线程并发地访问一个选择器的键的集合的时候存在任何问题,您可以采取一些步骤来合理地同步访问。在执行选择操作时,选择器在Selector 对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合,按照这样的顺序。已取消的键的集合也在选择过程的的第 1步和第 3 步之间保持同步(当与已取消的键的集合相关的通道被注销时)。
在多线程的场景中,如果您需要对任何一个键的集合进行更改,不管是直接更改还是其他操作带来的副作用,您都需要首先以相同的顺序,在同一对象上进行同步。锁的过程是非常重要的。如果竞争的线程没有以相同的顺序请求锁,就将会有死锁的潜在隐患。如果您可以确保否其他线程不会同时访问选择器,那么就不必要进行同步了。
Selector 类的close( )方法与slect( )方法的同步方式是一样的,因此也有一直阻塞的可能性。在选择过程还在进行的过程中,所有对close( )的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。在后面的情况下,执行选择的线程将会在执行关闭的线程获得锁是立即被唤醒,并关闭选择器。
异步关闭能力:任何时候都有可能关闭一个通道或者取消一个选择键。除非您采取步骤进行同步,否则键的状态及相关的通道将发生意料之外的改变。一个特定的键的集合中的一个键的存在并不保证键仍然是有效的,或者它相关的通道仍然是打开的。
关闭通道的过程不应该是一个耗时的操作。NIO 的设计者们特别想要阻止这样的可能性:一个线程在关闭一个处于选择操作中的通道时,被阻塞于无限期的等待。当一个通道关闭时,它相关的键也就都被取消了。这并不会影响正在进行的 select( ),但这意味着在您调用 select( )之前仍然是有效的键,在返回时可能会变为无效。您总是可以使用由选择器的 selectKeys( ) 方法返回的已选择的键的集合:请不要自己维护键的集合。理解选择过程,对于避免遇到问题而言是非常重要的。
选择过程的可扩展性:我多次提到选择器可以简化用单线程同时管理多个可选择通道的实现。使用一个线程来为多个通道提供服务,通过消除管理各个线程的额外开销,可能会降低复杂性并可能大幅提升性能。但只使用一个线程来服务所有可选择的通道是否是一个好主意呢?这要看情况。
对单CPU 的系统而言这可能是一个好主意,因为在任何情况下都只有一个线程能够运行。通过消除在线程之间进行上下文切换带来的额外开销,总吞吐量可以得到提高。但对于一个多 CPU的系统呢?在一个有 n 个CPU 的系统上,当一个单一的线程线性地轮流处理每一个线程时,可能有n-1 个cpu 处于空闲状态。
那么让不同道请求不同的服务类的办法如何?想象一下,如果一个应用程序为大量的分布式的传感器记录信息。每个传感器在服务线程遍历每个就绪的通道时需要等待数秒钟。这在响应时间不重要时是可以的。但对于高优先级的连接(如操作命令),如果只用一个线程为所有通道提供服务,将不得不在队列中等待。不同的应用程序的要求也是不同的。您采用的策略会受到您尝试解决的问题的影响。
在第一个场景中,如果您想要将更多的线程来为通道提供服务,请抵抗住使用多个选择器的欲望。在大量通道上执行就绪选择并不会有很大的开销,大多数工作是由底层操作系统完成的。管理多个选择器并随机地将通道分派给它们当中的一个并不是这个问题的合理的解决方案。这只会形成这个场景的一个更小的版本。
一个更好的策略是对所有的可选择通道使用一个选择器,并将对就绪通道的服务委托给其他线程。您只用一个线程监控通道的就绪状态并使用一个协调好的工作线程池来处理共接收到的数据。根据部署的条件,线程池的大小是可以调整的(或者它自己进行动态的调整)。对可选择通道的管理仍然是简单的,而简单的就是好的。
第二个场景中,某些通道要求比其他通道更高的响应速度,可以通过使用两个选择器来解决:一个为命令连接服务,另一个为普通连接服务。但这种场景也可以使用与第一个场景十分相似的办法来解决。与将所有准备好的通道放到同一个线程池的做法不同,通道可以根据功能由不同的工作线程来处理。它们可能可以是日志线程池,命令/控制线程池,状态请求线程池,等等。
下例 的代码是上例的一般性的选择循环的扩展。它覆写了 readDataFromSocket( ) 方法,并使用线程池来为准备好数据用于读取的通道提供服务。与在主线程中同步地读取数据不同,这个版本的实现将SelectionKey对象传递给为其服务的工作线程。
使用线程池来为通道提供服务
package com.ronsoft.books.nio.channels;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.SelectionKey;
import java.util.List;
import java.util.LinkedList;
import java.io.IOException;
/**
* Specialization of the SelectSockets class which uses a thread pool to service
* channels. The thread pool is an ad -hoc implementation quickly lashed together
* in a few hours for demonstration purposes. It's definitely not production
* quality.
*
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class SelectSocketsThreadPool extends SelectSockets {
private static final int MAX_THREADS = 5;
private ThreadPool pool = new ThreadPool(MAX_THREADS);
// -------------------------------------------------------------
public static void main(String[] argv) throws Exception {
new SelectSocketsThreadPool().go(argv);
}
// -------------------------------------------------------------
/**
* Sample data handler method for a channel with data ready to read. This
* method is invoked from the go() method in the parent class. This handler
* delegates to a worker thread in a thread pool to service the channel,
* then returns immediately.
*
* @param key
* A SelectionKey object representing a channel determined by the
* selector to be ready for reading. If the channel returns an
* EOF condition, it is closed here, which automatically
* invalidates the associated key. The selector will then de
* -register the channel on the next select call.
*/
protected void readDataFromSocket(SelectionKey key) throws Exception {
WorkerThread worker = pool.getWorker();
if (worker == null) {
// No threads available. Do nothing. The selection
// loop will keep calling this method until a
// thread becomes available. This design could
// be improved.
return;
}
// Invoking this wakes up the worker thread, then returns
worker.serviceChannel(key);
}
// ---------------------------------------------------------------
/**
* A very simple thread pool class. The pool size is set at construction
* time and remains fixed. Threads are cycled through a FIFO idle queue.
*/
private class ThreadPool {
List<WorkerThread> idle = new LinkedList<WorkerThread>();
ThreadPool(int poolSize) {
// Fill up the pool with worker threads
for (int i = 0; i < poolSize; i++) {
WorkerThread thread = new WorkerThread(this);
// Set thread name for debugging. Start it.
thread.setName("Worker" + (i + 1));
thread.start();
idle.add(thread);
}
}
/**
* Find an idle worker thread, if any. Could return null.
*/
WorkerThread getWorker() {
WorkerThread worker = null;
synchronized (idle) {
if (idle.size() > 0) {
worker = (WorkerThread) idle.remove(0);
}
}
return (worker);
}
/**
* Called by the worker thread to return itself to the idle pool.
*/
void returnWorker(WorkerThread worker) {
synchronized (idle) {
idle.add(worker);
}
}
}
/**
* A worker thread class which can drain channels and echo-back the input.
* Each instance is constructed with a reference to the owning thread pool
* object. When started, the thread loops forever waiting to be awakened to
* service the channel associated with a SelectionKey object. The worker is
* tasked by calling its serviceChannel() method with a SelectionKey
* object. The serviceChannel() method stores the key reference in the
* thread object then calls notify() to wake it up. When the channel has
*
* been drained, the worker thread returns itself to its parent pool.
*/
private class WorkerThread extends Thread {
private ByteBuffer buffer = ByteBuffer.allocate(1024);
private ThreadPool pool;
private SelectionKey key;
WorkerThread(ThreadPool pool) {
this.pool = pool;
}
// Loop forever waiting for work to do
public synchronized void run() {
System.out.println(this.getName() + " is ready");
while (true) {
try {
// Sleep and release object lock
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
// Clear interrupt status
this.interrupted();
}
if (key == null) {
continue; // just in case
}
System.out.println(this.getName() + " has been awakened");
try {
drainChannel(key);
} catch (Exception e) {
System.out.println("Caught '" + e + "' closing channel");
// Close channel and nudge selector
try {
key.channel().close();
} catch (IOException ex) {
ex.printStackTrace();
}
key.selector().wakeup();
}
key = null;
// Done. Ready for more. Return to pool
this.pool.returnWorker(this);
}
}
/**
* Called to initiate a unit of work by this worker thread on the
* provided SelectionKey object. This method is synchronized, as is the
* run() method, so only one key can be serviced at a given time.
* Before waking the worker thread, and before returning to the main
* selection loop, this key's interest set is updated to remove OP_READ.
* This will cause the selector to ignore read-readiness for this
* channel while the worker thread is servicing it.
*/
synchronized void serviceChannel(SelectionKey key) {
this.key = key;
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
this.notify(); // Awaken the thread
}
/**
*
* The actual code which drains the channel associated with the given
* key. This method assumes the key has been modified prior to
* invocation to turn off selection interest in OP_READ. When this
* method completes it re-enables OP_READ and calls wakeup() on the
* selector so the selector will resume watching this channel.
*/
void drainChannel(SelectionKey key) throws Exception {
SocketChannel channel = (SocketChannel) key.channel();
int count;
buffer.clear(); // Empty buffer
// Loop while data is available; channel is nonblocking
while ((count = channel.read(buffer)) > 0) {
buffer.flip(); // make buffer readable
// Send the data; may not go all at once
while (buffer.hasRemaining()) {
channel.write(buffer);
}
// WARNING: the above loop is evil.
// See comments in superclass.
buffer.clear(); // Empty buffer
}
if (count < 0) {
// Close channel on EOF; invalidates the key
channel.close();
return;
}
// Resume interest in OP_READ
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
// Cycle the selector so this key is active again
key.selector().wakeup();
}
}
}
由于执行选择过程的线程将重新循环并几乎立即再次调用select( ) ,键的interest 集合将被修改,并将 interest (感兴趣的操作)从读取就绪(read-rreadiness) 状态中移除。这将防止选择器重复地调用readDataFromSocket( ) (因为通道仍然会准备好读取数据,直到工作线程从它那里读取数据)。当工作线程结束为通道提供的服务时,它将再次更新键的 ready 集合,来将interest 重新放到读取就绪集合中。它也会在选择器上显式地调用 wakeup( )。如果主线程在 select( ) 中被阻塞,这将使它继续执行。这个选择循环会再次执行一个轮回(可能什么也没做)并带着被更新的键重新进入select( )。
总结:我们介绍了NIO 最强大的一面。就绪选择对大规模、高容量的服务器端应用程序来说是非常必要的。将这种能力补充到 Java 平台中,意味着企业级 Java 应用程序可以和用其他编程语言编写的有可比性的应用程序一较高下了。关键概念如下:
就绪选择相关类(Selector classes)
Selector ,SelectableChannel和SelectionKey这三个类组成了使得在 Java 平台上进行就绪检查变得可行的三驾马车。
SelectionKey 类封装了SelectableChannel对象和 Selector 之间的关系。
选择器请求操作系统决定那个注册到给定选择器上的通道已经准备好指定感兴趣的 I/O 操作。
异步关闭能力(Asynchronous closability)
我们接触了关于异步关闭选择器和通道的问题。
多线程(Multithreading)
我们探讨了怎样将多线程用于为可选择通道提供服务,而不必借助多个选择器对象来实现。 选择器为Java 服务器应用程序提供了强有力的保证。随着这种新的强大能力整合到商业服务器应用程序中去,服务器端的应用程序将更加可扩展,更可靠,并且响应速度更快。