Java NIO 选择器

第四章 选择器

            生活就是一系列猛然的醒悟。
                                              ——R. Van Winkle

在本章中,我们将探索选择器(selectors)。选择器提供选择执行已经就绪的任务的能力,这使得多元 I/O 成为可能。就像在第一章中描述的那样,就绪选择多元执行使得单线程能够有效率地同时管理多个 I/O 通道(channels)。 C/C++代码的工具箱中,许多年前就已经有 select()poll()这两个POSIX(可移植性操作系统接口)系统调用可供使用了。许过操作系统也提供相似的功能,但对Java 程序员来说,就绪选择功能直到 JDK 1.4 才成为可行的方案。对于主要的工作经验都是基于Java 环境的开发的程序员来说,之前可能还没有碰到过这种 I/O 模型。
为了更好地说明就绪选择,让我们回到第三章的带传送通道的银行的例子里。想象一下,一个有三个传送通道的银行。在传统的(非选择器)的场景里,想象一下每个银行的传送通道都有一个气动导管,传送到银行里它对应的出纳员的窗口,并且每一个窗口与其他窗口是用墙壁分隔开的。这意味着每个导管(通道)需要一个专门的出纳员(工作线程)。这种方式不易于扩展,而且也是十分浪费的。对于每个新增加的导管(通道),都需要一个新的出纳员,以及其他相关的经费,如表格、椅子、纸张的夹子(内存、 CPU 周期、上下文切换)等等。并且当事情变慢下来时,这些资源(以及相关的花费)大多数时候是闲置的。

现在想象一下另一个不同的场景,每一个气动导管(通道)都只与一个出纳员的窗口连接。这个窗口有三个槽可以放置运输过来的物品(数据缓冲区),每个槽都有一个指示器(选择键,selection key),当运输的物品进入时会亮起。同时想象一下出纳员(工作线程)有一个花尽量多的时间阅读《自己动手编写个人档案》一书的癖好。在每一段的最后,出纳员看一眼指示灯(调用select( )函数),来决定人一个通道是否已经就绪(就绪选择)。在传送带闲置时,出纳员(工作线程)可以做其他事情,但需要注意的时候又可以进行及时的处理。

虽然这种分析并不精确,但它描述了快速检查大量资源中的任意一个是否需要关注,而在某些东西没有准备好时又不必被迫等待的通用模式。这种检查并继续的能力是可扩展性的关键,它使得仅仅使用单一的线程就可以通过就绪选择来监控大量的通道。
选择器及相关的类就提供了这种 API,使得我们可以在通道上进行就绪选择。

4.1 选择器基础

掌握本章中讨论的主题,在某种程度上,比直接理解缓冲区和通道类更困难一些。这会复杂一些,因为涉及了三个主要的类,它们都会同时参与到整个过程中。如果您发现自己有些困惑,记录下来并先看其他内容。一旦您了解了各个部分是如何相互适应的,以及每个部分扮演的角色,您就会理解这些内容了。

我们会先从总体开始,然后分解为细节。您需要将之前创建的一个或多个可选择的通道注册到选择器对象中。一个表示通道和选择器的键将会被返回。选择键会记住您关心的通道。它们也会追踪对应的通道是否已经就绪。当您调用一个选择器对象的 select( )方法时,相关的键建会被更新,用来检查所有被注册到该选择器的通道。您可以获取一个键的集合,从而找到当时已经就绪的通道。通过遍历这些键,您可以选择出每个从上次您调用 select( )开始直到现在,已经就绪的通道。

这是在 3000 英尺高的地方看到的情景。现在,让我们看看在地面上(甚至地下)到底发生了什么。

现在,您可能已经想要跳到例 4-1,并快速地浏览一下代码了。通过在这里和那段代码之间的内容,您将学到这些新类是如何工作的。在掌握了前面的段落里的高层次的信息之后,您需要了解选择器模型是如何在实践中被使用的

从最基础的层面来看,选择器提供了询问通道是否已经准备好执行每个 I/0 操作的能力。例如,我们需要了解一个 SocketChannel 对象是否还有更多的字节需要读取,或者我们需要知道ServerSocketChannel 是否有需要准备接受的连接

在与 SelectableChannel 联合使用时,选择器提供了这种服务,但这里面有更多的事情需要去了解。就绪选择的真正价值在于潜在的大量的通道可以同时进行就绪状态的检查。调用者可以轻松地决定多个通道中的哪一个准备好要运行。有两种方式可以选择:被激发的线程可以处于休眠状态,直到一个或者多个注册到选择器的通道就绪或者它也可以周期性地轮询选择器,看看从上次检查之后,是否有通道处于就绪状态。如果您考虑一下需要管理大量并发的连接的网络服务器(webserver)的实现,就可以很容易地想到如何善加利用这些能力。

乍一看,好像只要非阻塞模式就可以模拟就绪检查功能,但实际上还不够。非阻塞模式同时还会执行您请求的任务,或指出它无法执行这项任务。这与检查它是否能够执行某种类型的操作是不同的。举个例子如果您试图执行非阻塞操作,并且也执行成功了,您将不仅仅发现 read()是可以执行的,同时您也已经读入了一些数据。就下来您就需要处理这些数据了。效率上的要求使得您不能将检查就绪的代码和处理数据的代码分离开来,至少这么做会很复杂。

即使简单地询问每个通道是否已经就绪的方法是可行的,在您的代码或一个类库的包里的某些代码需要遍历每一个候选的通道并按顺序进行检查的时候,仍然是有问题的。这会使得在检查每个通道是否就绪时都至少进行一次系统调用,这种代价是十分昂贵的,但是主要的问题是,这种检查不是原子性的。列表中的一个通道都有可能在它被检查之后就绪,但直到下一次轮询为止,您并不会觉察到这种情况。最糟糕的是,您除了不断地遍历列表之外将别无选择。您无法在某个您感兴趣的通道就绪时得到通知。

这就是为什么传统的监控多个 socket 的 Java 解决方案是为每个 socket 创建一个线程并使得线程可以在 read()调用中阻塞,直到数据可用。这事实上将每个被阻塞的线程当作了 socket 监控器,并将 Java 虚拟机的线程调度当作了通知机制。这两者本来都不是为了这种目的而设计的。程序员和 Java 虚拟机都为管理所有这些线程的复杂性和性能损耗付出了代价,这在线程数量的增长失控时表现得更为突出。

真正的就绪选择必须由操作系统来做。操作系统的一项最重要的功能就是处理 I/O 请求并通知各个线程它们的数据已经准备好了。选择器类提供了这种抽象,使得 Java 代码能够以可移植的方式,请求底层的操作系统提供就绪选择服务。让我们看一下 java.nio.channels 包中处理就绪选择的特定的类。

4.1.1 选择器,可选择通道和选择键类

现在,您也许还对这些用于就绪选择的 Java 成员感到困惑。让我们来区分这些活动的零件并了解它们是如何交互的吧。图 4-1 的 UML 图使得情形看起来比真实的情况更为复杂了。看看图 4-2,然后您会发现实际上只有三个有关的类 API,用于执行就绪选择:

选择器(Selector)

选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

可选择通道(SelectableChannel)

这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。 FileChannel 对象不是可选择的,因为它们没有继承 SelectableChannel(见图 4-2)。所有 socket 通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。 SelectableChannel 可以被注册到 Selector 对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

选择键(SelectionKey)

选 择 键 封 装 了 特 定 的 通 道 与 特 定 的 选 择 器 的 注 册 关 系 。 选 择 键 对 象 被SelectableChannel.register( ) 返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

图 4-1. 就绪选择相关类的继承关系图

152359_we8i_3101476.png

让我们看看 SelectableChannel 的相关 API 方法

public abstract class SelectableChannel extends AbstractChannel
implements Channel
{
// This is a partial API listing
public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException;
public abstract SelectionKey register (Selector sel, int ops, Object att) throws ClosedChannelException;
public abstract boolean isRegistered( );
public abstract SelectionKey keyFor (Selector sel);
public abstract int validOps( );
public abstract void configureBlocking (boolean block) throws IOException;
public abstract boolean isBlocking( );
public abstract Object blockingLock( );
}

非阻塞特性与多元执行特性的关系是十分密切的——以至于 java.nio 的架构将两者的 API放到了一个类中。

我们已经探讨了如何用上面列出的 SelecableChannel 的最后三个方法来配置并检查通道的阻塞模式(详细的探讨请参考 3.5.1 小节)。通道在被注册到一个选择器上之前,必须先设置为非阻塞模式(通过调用 configureBlocking(false))。

图 4-2. 就绪选择相关类的关系
152949_hkgw_3101476.png

调用可选择通道的 register( )方法会将它注册到一个选择器上。如果您试图注册一个处于阻塞状态的通道, register( )将抛出未检查的 IllegalBlockingModeException 异常。此外,通道一旦被注册,就不能回到阻塞状态。试图这么做的话,将在调用 configureBlocking( )方法时将抛出IllegalBlockingModeException 异常。

并且,理所当然地,试图注册一个已经关闭的 SelectableChannel 实例的话,也将抛出ClosedChannelException 异常,就像方法原型指示的那样。

在我们进一步了解 register( )和 SelectableChannel 的其他方法之前,让我们先了解一下Selector 类的 API,以确保我们可以更好地理解这种关系:

public abstract class Selector
{
public static Selector open( ) throws IOException
public abstract boolean isOpen( );
public abstract void close( ) throws IOException;
public abstract SelectionProvider provider( );
public abstract int select( ) throws IOException;
public abstract int select (long timeout) throws IOException;
public abstract int selectNow( ) throws IOException;
public abstract void wakeup( );
public abstract Set keys( );
public abstract Set selectedKeys( );
}

尽管 SelectableChannel 类上定义了 register( )方法,还是应该将通道注册到选择器上,而不是另一种方式。选择器维护了一个需要监控的通道的集合。一个给定的通道可以被注册到多于一个 的 选 择 器 上 , 而 且 不 需 要 知 道 它 被 注 册 了 那 个 Selector 对 象 上 。 将 register( ) 放 在SelectableChannel 上而不是 Selector 上,这种做法看起来有点随意。它将返回一个封装了两个对象的关系的选择键对象。重要的是要记住选择器对象控制了被注册到它之上的通道的选择过程

public abstract class SelectionKey
{
public static final int OP_READ
public static final int OP_WRITE
public static final int OP_CONNECT
public static final int OP_ACCEPT
public abstract SelectableChannel channel( );
public abstract Selector selector( );
public abstract void cancel( );
public abstract boolean isValid( );
public abstract int interestOps( );
public abstract void interestOps (int ops);
public abstract int readyOps( );
public final boolean isReadable( )
public final boolean isWritable( )
public final boolean isConnectable( )
public final boolean isAcceptable( )
public final Object attach (Object ob)
public final Object attachment( )
}

153653_zNTC_3101476.png

对于键的 interest(感兴趣的操作)集合和 ready(已经准备好的操作)集合的解释是和特定的通道相关的。每个通道的实现,将定义它自己的选择键类。在 register( )方法中构造它并将它传递给所提供的选择器对象。
在下面的章节里,我们将了解关于这三个类的方法的更多细节。

4.1.2 建立选择器

现在您可能仍然感到困惑,您在前面的三个清单中看到了大量的方法,但无法分辨出它们具体做什么,或者它们代表了什么意思。在钻研所有这一切的细节之前,让我们看看一个经典的应用实例。它可以帮助我们将所有东西放到一个特定的上下文中去理解。

为了建立监控三个 Socket 通道的选择器,您需要做像这样的事情(参见图 4-2):

Selector selector = Selector.open( );
channel1.register (selector, SelectionKey.OP_READ);
channel2.register (selector, SelectionKey.OP_WRITE);
channel3.register (selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// Wait up to 10 seconds for a channel to become ready
readyCount = selector.select (10000);

这些代码创建了一个新的选择器,然后将这三个(已经存在的)socket 通道注册到选择器上,而且感兴趣的操作各不相同。

select( )方法在将线程置于睡眠状态,直到这些刚兴趣的事情中的操作中的一个发生或者 10 秒钟的时间过去。

现在让我们看看 Selector 的 API 的细节:

public abstract class Selector
{
// This is a partial API listing
public static Selector open( ) throws IOException
public abstract boolean isOpen( );
public abstract void close( ) throws IOException;
public abstract SelectionProvider provider( );
}

Selector 对象是通过调用静态工厂方法 open( )来实例化的。选择器不是像通道或流(stream)那样的基本 I/O 对象:数据从来没有通过它们进行传递。类方法 open( )向 SPI 发出请求,通过默认的 SelectorProvider 对象获取一个新的实例。通过调用一个自定义的 SelectorProvider对象的 openSelector( )方法来创建一个 Selector 实例也是可行的。您可以通过调用 provider( )方法来决定由哪个 SelectorProvider 对象来创建给定的 Selector 实例。大多数情况下,您不需要关心 SPI;只需要调用 open( )方法来创建新的 Selector 对象。在那些您必须处理它们的罕见的情况下,您可以参考在附录 B 中总结的通道的 SPI 包。

继续关于将 Select 作为 I/O 对象进行处理的话题的探讨:当您不再使用它时,需要调用 close( )方法来释放它可能占用的资源并将所有相关的选择键设置为无效。一旦一个选择器被关闭,试图调用它的大多数方法都将导致 ClosedSelectorException。注意 ClosedSelectorException是一个非检查(运行时的)错误。您可以通过 isOpen( )方法来测试一个选择器是否处于被打开的状态。

我们将结束对 Selector 的 API 的探讨,但现在先让我们看看如何将通道注册到选择器上。下面是一个之前章节中出现过的 SelectableChannel 的 API 的简化版本:
 

public abstract class SelectableChannel extends AbstractChannel
implements Channel
{
// This is a partial API listing
public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException;
public abstract SelectionKey register (Selector sel, int ops, Object att)
throws ClosedChannelException;
public abstract boolean isRegistered( );
public abstract SelectionKey keyFor (Selector sel);
public abstract int validOps( );
}

就像之前提到的那样, register( )方法位于 SelectableChannel 类,尽管通道实际上是被注册到选择器上的。您可以看到 register( )方法接受一个 Selector 对象作为参数,以及一个名为ops 的整数参数。第二个参数表示所关心的通道操作。这是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩。特定的操作比特值在 SelectonKey 类中被定义为 public static 字段。

在 JDK 1.4 中,有四种被定义的可选择操作:读(read),写(write),连接(connect)和接受(accept)。

并非所有的操作都在所有的可选择通道上被支持。例如, SocketChannel 不支持 accept。试图注册不支持的操作将导致 IllegalArgumentException您可以通过调用 validOps( )方法来获取特定的通道所支持的操作集合。我们可以在第三章中探讨的 socket 通道类中看到这些方法。

选择器包含了注册到它们之上的通道的集合。在任意给定的时间里,对于一个给定的选择器和一个给定的通道而言,只有一种注册关系是有效的。但是,将一个通道注册到多于一个的选择器上允许的。这么做的话,在更新 interest 集合为指定的值的同时,将返回与之前相同的选择键。实际上,后续的注册都只是简单地将与之前的注册关系相关的键进行更新(见 4.2 小节)。

一个例外的情形是当您试图将一个通道注册到一个相关的键已经被取消的选择器上,而通道仍然处于被注册的状态的时候。通道不会在键被取消的时候立即注销(注销是:selector中的SekectionKey被删除)。直到下一次操作发生为止,它们仍然会处于被注册的状态(见 4.3 小节)。在这种情况下,未检查的 CancelledKeyException将会被抛出。请务必在键可能被取消的情况下检查 SelectionKey 对象的状态(key.isValid())。

在之前的清单中,您可能已经注意到了 register( )的第二个版本,这个版本接受 object 参数。这是一个方便的方法,可以传递您提供的对象引用,在调用新生成的选择键的 attach( )方法时会将这个对象引用返回给您。我们将会在下一节更进一步地了解 SelectionKey 的 API。

一个单独的通道对象可以被注册到多个选择器上。可以调用 isRegistered( )方法来检查一个通道是否被注册到任何一个选择器上。这个方法没有提供关于通道被注册到哪个选择器上的信息,而只能知道它至少被注册到了一个选择器上。 此外,在一个键被取消之后,直到通道被注销为止,可能有时间上的延迟。这个方法只是一个提示,而不是确切的答案。

任何一个通道和选择器的注册关系都被封装在一个 SelectionKey 对象中。 channel的keyFor( )方法将返回与该通道和指定的选择器相关的键。如果通道被注册到指定的选择器上,那么相关的键将被返回。如果它们之间没有注册关系,那么将返回 null。

4.2 使用选择键

让我们看看 SelectionKey 类的 API:

package java.nio.channels;
public abstract class SelectionKey
{
public static final int OP_READ
public static final int OP_WRITE
public static final int OP_CONNECT
public static final int OP_ACCEPT
public abstract SelectableChannel channel( );
public abstract Selector selector( );
public abstract void cancel( );
public abstract boolean isValid( );
public abstract int interestOps( );
public abstract void interestOps (int ops);
public abstract int readyOps( );
public final boolean isReadable( )
public final boolean isWritable( )
public final boolean isConnectable( )
public final boolean isAcceptable( )
public final Object attach (Object ob)
public final Object attachment( )
}

就像之前提到的那样,一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关 系 。 您 可 以 看 到 前 两 个 方 法 中 反 映 了 这 种 关 系 。 channel( ) 方 法 返 回 与 该 键 相 关 的SelectableChannel 对象,而 selector( )则返回相关的 Selector 对象。这没有什么令人惊奇的。

键对象表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用 SelectionKey对象的 cancel( )方法。可以通过调用 isValid( )方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效(参见 4.3 节)。当再次调用 select( )方法时(或者一个正在进行的 select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销(在Selector中注销,但channel不会自动关闭),而新的SelectionKey 将被返回。

当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出 CancelledKeyException

一个 SelectionKey 对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(instrest 集合),另一个表示通道准备好要执行的操作 ready 集合)。当前的 interest 集合可以通过调用键对象的 interestOps( )方法来获取。最初,这应该是通道被注册时传进来的值。这个 interset 集合永远不会被选择器改变,但您可以通过调用 interestOps( )方法并传入一个新的比特掩码参数来改变它。 interest 集合也可以通过将通道注册到选择器上来改变(实际上使用一种迂回的方式调用 interestOps( )),就像 4.1.2 小节中描的那样。当相关的 Selector 上的 select( )操作正在进行时改变键的 interest 集合,不会影响那个正在进行的选择操作。所有更改将会在 select( )的下一个调用中体现出来。

可以通过调用键的 readyOps( )方法来获取相关的通道的已经就绪的操作。 ready 集合是 interest集合的子集,并且表示了 interest 集合中从上次调用 select( )以来已经就绪的那些操作。例如,下面的代码测试了与键关联的通道是否就绪。如果就绪,就将数据读取出来,写入一个缓冲区,并将它送到一个 consumer(消费者)方法中。

if ((key.readyOps( ) & SelectionKey.OP_READ) != 0)
{
myBuffer.clear( );
key.channel().read (myBuffer);
doSomethingWithBuffer (myBuffer.flip( ));
}

就像之前提到过的那样,有四个通道操作可以被用于测试就绪状态。您可以像上面的代码那样,通过测试比特掩码来检查这些状态,但 SelectionKey 类定义了四个便于使用的布尔方法来为您测试这些比特值: isReadable( )isWritable( )isConnectable( ), 和 isAcceptable( )。每一个方法都与使用特定掩码来测试 readyOps( )方法的结果的效果相同。例如:

if (key.isWritable( ))
等价于:
if ((key.readyOps( ) & SelectionKey.OP_WRITE) != 0)

这四个方法在任意一个 SelectionKey 对象上都能安全地调用。不能在一个通道上注册一个它不支持的操作,这种操作也永远不会出现在 ready 集合中。调用一个不支持的操作将总是返回 false,因为这种操作在该通道上永远不会准备好。

需要注意的是,通过相关的选择键的 readyOps( )方法返回的就绪状态指示只是一个提示,不是保证。底层的通道在任何时候都会不断改变。其他线程可能在通道上执行操作并影响它的就绪状态。同时,操作系统的特点也总是需要考虑的。

182844_Qvdn_3101476.png

您可能会从 SelectionKey 的 API 中注意到尽管有获取 ready 集合的方法,但没有重新设置那个集合的成员方法。事实上,您不能直接改变键的 ready 集合。在下一节里,也就是描述选择过程时,我们将会看到选择器和键是如何进行交互,以提供实时更新的就绪指示的。

让我们试验一下 SelectionKey 的 API 中剩下的两个方法:

public abstract class SelectionKey
{
// This is a partial API listing
public final Object attach (Object ob)
public final Object attachment( )
}

这两个方法允许您在键上放置一个“附件”,并在后面获取它。这是一种允许您将任意对象与键关联的便捷的方法。这个对象可以引用任何对您而言有意义的对象,例如业务对象、会话句柄、其他通道等等。这将允许您遍历与选择器相关的键,使用附加在上面的对象句柄作为引用来获取相关的上下文。

attach( )方法将在键对象中保存所提供的对象的引用。 SelectionKey 类除了保存它之外,不会将它用于任何其他用途。任何一个之前保存在键中的附件引用都会被替换。可以使用 null 值来清除附件。可以通过调用 attachment( )方法来获取与键关联的附件句柄。如果没有附件,或者显式地通过 null 方法进行过设置,这个方法将返回 null。

191240_orpd_3101476.png

SelectableChannel 类的一个 register( )方法的重载版本接受一个 Object 类型的参数。这是一个方便您在注册时附加一个对象到新生成的键上的方法。以下代码:

SelectionKey key =channel.register (selector, SelectionKey.OP_READ, myObject);
等价于:
SelectionKey key = channel.register (selector, SelectionKey.OP_READ);
key.attach (myObject);

关于 SelectionKey 的最后一件需要注意的事情是并发性。总体上说, SelectionKey 对象是线程安全的,但知道修改 interest 集合的操作是通过 Selector 对象进行同步的是很重要的。这可能会导致 interestOps( )方法的调用会阻塞不确定长的一段时间。选择器所使用的锁策略(例如是否在整个选择过程中保持这些锁)是依赖于具体实现的。幸好,这种多元处理能力被特别地设计为可以使用单线程来管理多个通道。被多个线程使用的选择器也只会在系统特别复杂时产生问题。坦白地说,如果您在多线程中共享选择器时遇到了同步的问题,也许您需要重新思考一下您的设计。
我们已经探讨了 SelectionKey 的 API,但我们还没有谈完选择键的一切——远远没有。让我们进一步了解如何使用选择器管理键吧。

4.3 使用选择器

既然我们已经很好地掌握了了各种不同类以及它们之间的关联,那么现在让我们进一步了解Selector 类,也就是就绪选择的核心。这里是 Selector 类的可用的 API。在 4.1.2 小节中,我们已经看到如何创建新的选择器,那么那些方法还剩下:

4.3.1 选择过程

在详细了解 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.已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销(注销的过程是channel与selector解绑的过程)。这个步骤结束后,已取消的键的集合将是空的。

2.已注册的键的集合中的键的 interest 集合将被检查。在这个步骤中的检查执行过后,对interest 集合的改动不会影响剩余的检查过程。

一旦就绪条件被定下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态。依赖于特定的 select( )方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。

直到系统调用完成为止,这个过程可能会使得调用线程睡眠一段时间,然后当前某个通道的就绪状态将确定下来。对于那些还没准备好的通道将不会执行任何的操作。对于那些操作系统指示至少已经准备好 interest 集合中的一种操作的通道,将执行以下两种操作中的一种:

a.如果通道的键还没有处于已选择的键的集合中,那么键的 ready 集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。
b.否则,也就是键在已选择的键的集合中。键的 ready 集合将被表示操作系统发现的当前已经准备好的操作的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的 ready 集合是与之前的 ready 集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的 ready 集合将是累积的。比特位只会被设置,不会被清理。

3.步骤 2 可能会花费很长时间,特别是所激发的线程处于休眠状态时。与该选择器相关的键可能会同时被取消。当步骤 2 结束时,步骤 1 将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注销

4.select 操作返回的值是 ready 集合在步骤 2 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个 select( )调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是 0。

使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化。注销通道是一个潜在的代价很高的操作,这可能需要重新分配资源(请记住,键是与通道相关的,并且可能与它们相关的通道对象之间有复杂的交互)。清理已取消的键,并在选择操作之前和之后立即注销通道,可以消除它们可能正好在选择的过程中执行的潜在棘手问题。这是另一个兼顾健壮性的折中方案。

Selector 类的 select( )方法有以下三种不同的形式:

这三种 select 的形式,仅仅在它们在所注册的通道当前都没有就绪时,是否阻塞的方面有所不同。最简单的没有参数的形式可以用如下方式调用:

这种调用在没有通道就绪时将无限阻塞。一旦至少有一个已注册的通道就绪,选择器的选择键就会被更新,并且每个就绪的通道的 ready 集合也将被更新。返回值将会是已经确定就绪的通道的数目。正常情况下,这些方法将返回一个非零的值,因为直到一个通道就绪前它都会阻塞。但是它也可以返回非 0 值,如果选择器的 wakeup( )方法被其他线程调用。

有时您会想要限制线程等待通道就绪的时间。这种情况下,可以使用一个接受一个超时参数的select( )方法的重载形式:

这种调用与之前的例子完全相同,除了如果在您提供的超时时间(以毫秒计算)内没有通道就绪时,它将返回 0。如果一个或者多个通道在时间限制终止前就绪,键的状态将会被更新,并且方法会在那时立即返回。将超时参数指定为 0 表示将无限期等待,那么它就在各个方面都等同于使用无参数版本的 select( )了。

就绪选择的第三种也是最后一种形式是完全非阻塞的:

int n = selector.selectNow( );

selectNow()方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回 0。

4.3.2 停止选择过程

Selector 的 API 中的最后一个方法,wakeup( ),提供了使线程从被阻塞的 select( )方法中优雅地退出的能力:

public abstract class Selector
{
// This is a partial API listing
public abstract void wakeup( );
}

有三种方式可以唤醒在 select( )方法中睡眠的线程:

调用 wakeup( )

调用 Selector 对象的 wakeup( )方法将使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有在进行中的选择,那么下一次对 select( )方法的一种形式的调用将立即返回。后续的选择操作将正常进行。在选择操作之间多次调用 wakeup( )方法与调用它一次没有什么不同

有时这种延迟的唤醒行为并不是您想要的。您可能只想唤醒一个睡眠中的线程,而使得后续的选择继续正常地进行。您可以通过在调用 wakeup( )方法后调用 selectNow( )方法来绕过这个问题。尽管如此,如果您将您的代码构造为合理地关注于返回值和执行选择集合,那么即使下一个 select( )方法的调用在没有通道就绪时就立即返回,也应该不会有什么不同。不管怎么说,您应该为可能发生的事件做好准备。

调用 close( )

如果选择器的 close( )方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,就像wakeup( )方法被调用了一样。与选择器相关的通道将被注销, 而键将被取消。

调用 interrupt( )

如果睡眠中的线程的 interrupt( )方法被调用,它的返回状态将被设置。如果被唤醒的线程之后将试图在通道上执行 I/O 操作,通道将立即关闭,然后线程将捕捉到一个异常。这是由于在第三章中已经探讨过的通道的中断语义。使用 wakeup( )方法将会优雅地将一个在 select( )方法中睡眠的线程唤醒。如果您想让一个睡眠的线程在直接中断之后继续执行,需要执行一些步骤来清理中断状态(参见 Thread.interrupted( )的相关文档)。

Selector 对象将捕捉 InterruptedException 异常并调用 wakeup( )方法。

请注意这些方法中的任意一个都不会关闭任何一个相关的通道。中断一个选择器与中断一个通道是不一样的(参见 3.3 节)。选择器不会改变任意一个相关的通道,它只会检查它们的状态。当一个在 select( )方法中睡眠的线程中断时,对于通道的状态而言,是不会产生歧义的。

4.3.3 管理选择键

既然我们已经理解了问题的各个部分是怎样结合在一起的,那么是时候看看它们在正常的使用中是如何交互的了。为了有效地利用选择器和键提供的信息,合理地管理键是非常重要的。

选择是累积的。一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个键。并且,一旦一个键处于已选择的键的集合中,这个键的 ready 集合将只会被设置,而不会被清理。乍一看,这好像会引起麻烦,因为选择操作可能无法表现出已注册的通道的正确状态。它提供了极大的灵活性,但把合理地管理键以确保它们表示的状态信息不会变得陈旧的任务交给了程序员。

合理地使用选择器的秘诀是理解选择器维护的选择键集合所扮演的角色。(参见 4.3.1 小节,特别是选择过程的第二步。)最重要的部分是当键已经不再在已选择的键的集合中时将会发生什么。当通道上的至少一个感兴趣的操作就绪时,键的 ready 集合就会被清空,并且当前已经就绪的操作将会被添加到 ready 集合中。该键之后将被添加到已选择的键的集合中。

这种框架提供了很多灵活性。通常的做法是在选择器上调用一次 select 操作(这将更新已选择的键的集合),然后遍历 selectKeys( )方法返回的键的集合。在按顺序进行检查每个键的过程中,相关的通道也根据键的就绪集合进行处理。然后键将从已选择的键的集合中被移除(通过在 Iterator对象上调用 remove( )方法),然后检查下一个键。完成后,通过再次调用 select( )方法重复这个循环。例 4-1 中的代码是典型的服务器的例子。

例 4-1. 使用 select( )来为多个通道提供服务

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: zhubo
 * Date: 2018-01-18
 * Time: 10:50
 */
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 port
            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();
            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();
            }
        }
    }
// ----------------------------------------------------------

    /**
     * Register the given channel with the given selector for the given
     * operations of interest
     */
    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 nonblocking
        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);
    }
}

例 4-1 实现了一个简单的服务器。它创建了 ServerSocketChannel 和 Selector 对象,并将通道注册到选择器上。我们不在注册的键中保存服务器 socket 的引用,因为它永远不会被注销。这个无限循环在最上面先调用了 select( ),这可能会无限期地阻塞。当选择结束时,就遍历选择键并检查已经就绪的通道。

如果一个键指示与它相关的通道已经准备好执行一个 accecpt( )操作,我们就通过键获取关联的通道,并将它转换为 SeverSocketChannel 对象。我们都知道这么做是安全的,因为只有ServerSocketChannel 支持 OP_ACCEPT 操作。我们也知道我们的代码只把对一个单一的ServerSocketChannel 对象的 OP_ACCEPT 操作进行了注册。通过对服务器 socket 通道的引用,我们调用了它的 accept( )方法,来获取刚到达的 socket 的句柄。返回的对象的类型是SocketChannel,也是一个可选择的通道类型。这时,与创建一个新线程来从新的连接中读取数据不同,我们只是简单地将 socket 同多注册到选择器上。我们通过传入 OP_READ 标记,告诉选择器我们关心新的 socket 通道什么时候可以准备好读取数据。

如果键指示通道还没有准备好执行 accept( ),我们就检查它是否准备好执行 read( )。任何一个这么指示的 socket 通道一定是之前 ServerSocketChannel 创建的 SocketChannel 对象之一,并且被注册为只对读操作感兴趣。对于每个有数据需要读取的 socket 通道,我们调用一个公共的方法来读取并处理这个带有数据的 socket。需要注意的是这个公共方法需要准备好以非阻塞的方式处理 socket 上的不完整的数据。它需要迅速地返回,以其他带有后续输入的通道能够及时地得到处理。例 4-1 中只是简单地对数据进行响应,将数据写回 socket,传回给发送者。在循环的底部,我们通过调用 Iterator(迭代器)对象的 remove()方法,将键从已选择的键的集合中移除。键可以直接从 selectKeys()返回的 Set 中移除,但同时需要用 Iterator 来检查集合,您需要使用迭代器的 remove()方法来避免破坏迭代器内部的状态。

4.3.4 并发性

选择器对象是线程安全的,但它们包含的键集合不是。通过 keys( )selectKeys( )返回的键的集合是 Selector 对象内部的私有的 Set 对象集合的直接引用。这些集合可能在任意时间被改变。已注 册 的 键 的 集 合 是 只 读 的 。 如 果 您 试 图 修 改 它 , 那 么 您 得 到 的 奖 品 将 是 一 个java.lang.UnsupportedOperationException,但是当您在观察它们的时候,它们可能发生了改变的话,您仍然会遇到麻烦。 Iterator 对象是快速失败的(fail-fast):如果底层的 Set 被改变了,它们将会抛出 java.util.ConcurrentModificationException,因此如果您期望在多个线程间共享选择器和/或键,请对此做好准备。您可以直接修改选择键,但请注意您这么做时可能会彻底破坏另一个线程的 Iterator。

如果在多个线程并发地访问一个选择器的键的集合的时候存在任何问题,您可以采取一些步骤来合理地同步访问。在执行选择操作时,选择器在 Selector 对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合,按照这样的顺序。已取消的键的集合也在选择过程的的第 1步和第 3 步之间保持同步(当与已取消的键的集合相关的通道被注销时)。

在多线程的场景中,如果您需要对任何一个键的集合进行更改,不管是直接更改还是其他操作带来的副作用,您都需要首先以相同的顺序,在同一对象上进行同步。锁的过程是非常重要的。如果竞争的线程没有以相同的顺序请求锁,就将会有死锁的潜在隐患。如果您可以确保否其他线程不会同时访问选择器,那么就不必要进行同步了。

Selector 类的 close( )方法与 slect( )方法的同步方式是一样的,因此也有一直阻塞的可能性。在选择过程还在进行的过程中,所有对 close( )的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。在后面的情况下,执行选择的线程将会在执行关闭的线程获得锁是立即被唤醒,并关闭选择器(参见 4.3.2 小节) 。

一个更好的策略是对所有的可选择通道使用一个选择器,并将对就绪通道的服务委托给其他线程。您只用一个线程监控通道的就绪状态并使用一个协调好的工作线程池来处理共接收到的数据。根据部署的条件,线程池的大小是可以调整的(或者它自己进行动态的调整)。对可选择通道的管理仍然是简单的,而简单的就是好的。

第二个场景中,某些通道要求比其他通道更高的响应速度,可以通过使用两个选择器来解决:一个为命令连接服务,另一个为普通连接服务。但这种场景也可以使用与第一个场景十分相似的办法来解决。与将所有准备好的通道放到同一个线程池的做法不同,通道可以根据功能由不同的工作线程来处理。它们可能可以是日志线程池,命令/控制线程池,状态请求线程池,等等。

例 4-2 的代码是例 4-1 的一般性的选择循环的扩展。它覆写了 readDataFromSocket( )方法,并使用线程池来为准备好数据用于读取的通道提供服务。与在主线程中同步地读取数据不同,这个版本的实现将 SelectionKey 对象传递给为其服务的工作线程。这个案例比较6

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: zhubo
 * Date: 2018-01-18
 * Time: 10:50
 */
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 port
            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();
            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();
            }
        }
    }
// ----------------------------------------------------------

    /**
     * Register the given channel with the given selector for the given
     * operations of interest
     */
    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 nonblocking
        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);
    }
}
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: zhubo
 * Date: 2018-01-18
 * Time: 11:07
 */
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.
     */
    @Override
    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 idle = new LinkedList();
        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
        @Override
        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
                    Thread.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( )

4.6 总结

在本章中,我们介绍了 NIO 最强大的一面。就绪选择对大规模、高容量的服务器端应用程序来说是非常必要的。将这种能力补充到 Java 平台中,意味着企业级 Java 应用程序可以和用其他编程语言编写的有可比性的应用程序一较高下了。本章中的关键概念如下:

就绪选择相关类(Selector classes)

Selector, SelectableChannel 和 SelectionKey 这三个类组成了使得在 Java 平台上进行就绪检查变得可行的三驾马车。在 4.1 小节,我们看到了这些类相互之间的关联以及它们表示的含义。

选择键(Selection keys)

在 4.2 小节中,我们学到了更多关于选择键的知识以及如何使用它们。 SelectionKey 类封装了 SelectableChannel 对象和 Selector 之间的关系。

选择器(Selectors)

选择器请求操作系统决定那个注册到给定选择器上的通道已经准备好指定感兴趣的 I/O 操作。我们在 4.3 小节学习了怎样管理键集合并从 select( )的调用中返回。我们也探讨了一些与就绪选择相关的并发性问题。

异步关闭能力(Asynchronous closability)

在 4.1 小节中我们接触了关于异步关闭选择器和通道的问题。

多线程(Multithreading)

在 4.5 小节,我们探讨了怎样将多线程用于为可选择通道提供服务,而不必借助多个选择器对象来实现。

选择器为 Java 服务器应用程序提供了强有力的保证。随着这种新的强大能力整合到商业服务器应用程序中去,服务器端的应用程序将更加可扩展,更可靠,并且响应速度更快。

好了。我们已将结束了了解 java.nio 和它的子类的旅程。当请不要放下摄像机离开。我们还有更多不额外收费的奖品。重新登上公交车时请注意您的脚,下一站是正则表达式的迷人大陆。
 

 

转载于:https://my.oschina.net/LucasZhu/blog/1608043

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值