Java nio 入门教程详解(4)

更新时间:2023-07-18 08:54:01 阅读量: 实用文档 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

nio,详解

Java nio入门教程详解(三十)

Java 2013 年 8 月 21 日 暂无评论

第四章 选择器 在本章中,我们将探索选择器(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 选择器基础

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

nio,详解

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

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

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

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

ServerSocketChannel是否有需要准备接受的连接。

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

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

效率上的要求使得您不能将检查就绪的代码和处理数据的代码分离开来,至少这么做会很复杂。

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

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

nio,详解

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

让我们看一下java.nio.channels包中处理就绪选择的特定的类。

Java nio入门教程详解(三十一)

Java 2013 年 8 月 21 日 暂无评论

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

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

选择器(Selector)

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

有就绪的的通道。

可选择通道(SelectableChannel)

这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父

类。FileChannel对象不是可选择的,因为它们没有继承 SelectableChannel(见

图 4-2)。所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。

SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而

言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注

册一次。

选择键(SelectionKey)

选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被

SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键

包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已

经准备好的操作。

nio,详解

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

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

nio,详解

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

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

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

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

并且,理所当然地,试图注册一个已经关闭的SelectableChannel实例的话,也将抛出

ClosedChannelException异常,就像方法原型指示的那样。

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

nio,详解

尽管SelectableChannel类上定义了register()方法,还是应该将通道注册到选择器上,而不是另一种方式。选择器维护了一个需要监控的通道的集合。一个给定的通道可以被注册到多于一个的选择器上 ,而且不需要知道它被注册了那个Selector对象上 。将register()放在

SelectableChannel上而不是Selector上,这种做法看起来有点随意。它将返回一个封装了两个对象的关系的选择键对象。重要的是要记住选择器对象控制了被注册到它之上的通道的选择过程。

选择器才是提供管理功能的对象,而不是可选择通道对象。选择器对象对注册到它之上的通道执行就绪选择,并管理选择键。

nio,详解

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

在下面的章节里,我们将了解关于这三个类的方法的更多细节。

Java nio入门教程详解(三十二)

Java 2013 年 8 月 21 日 暂无评论

4.1.2 建立选择器

现在您可能仍然感到困惑,您在前面的三个清单中看到了大量的方法,但无法分辨出它们具体做什么,或

者它们代表了什么意思。在钻研所有这一切的细节之前,让我们看看一个经典的应用实例。它可以帮助我们将所有东西放到一个特定的上下文中去理解。

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

选择器才是提供管理功能的对象,而不是可选择通道对象。选择器对象对注册到它之上的通道执行就绪选择,并管理选择键。

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

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

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

nio,详解

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的简化版本:

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

nio,详解

在 JDK 1.4 中,有四种被定义的可选择操作:读(read),写(write),连接(connect)和接受(accept)。 并非所有的操作都在所有的可选择通道上被支持。例如,SocketChannel不支持accept。试图注册不支持的操作将导致 IllegalArgumentException。您可以通过调用validOps()方法来获取特定的通道所支持的操作集合。我们可以在第三章中探讨的socket通道类中看到这些方法。 选择器包含了注册到它们之上的通道的集合。在任意给定的时间里,对于一个给定的选择器和一个给定的通道而言,只有一种注册关系是有效的。但是,将一个通道注册到多于一个的选择器上允许的。这么做的话,在更新interest集合为指定的值的同时,将返回与之前相同的选择键。实际上,后续的注册都只是简单地将与之前的注册关系相关的键进行更新(见 4.2 小节)。

一个例外的情形是当您试图将一个通道注册到一个相关的键已经被取消的选择器上,而通道仍然处于被注册的状态的时候。通道不会在键被取消的时候立即注销。直到下一次操作发生为止,它们仍然会处于被注册的状态(见

4.3 小节)。在这种情况下,未检查的CancelledKeyException将会被抛出。请务必在键可能被取消的情况下检查SelectionKey对象的状态。

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

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

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

Java nio入门教程详解(三十三)

Java 2013 年 8 月 22 日 暂无评论

4.2 使用选择键

让我们看看SelectionKey类的API:

nio,详解

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

SelectableChannel对象,而selector()则返回相关的Selector对象。这没有什么令人惊奇的。

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

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

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

nio,详解

Selector上的select()操作正在进行时改变键的interest集合,不会影响那个正在进行的选择操作。所有更改将会在select()的下一个调用中体现出来。

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

就像之前提到过的那样,有四个通道操作可以被用于测试就绪状态。您可以像上面的代码那样,通过测试比特掩码来检查这些状态,

但SelectionKey类定义了四个便于使用的布尔方法来为您测试这些比特值:isReadable(),isWritable(),isConnectable(), 和 isAcceptable()。每一个方法都与使用特定掩码来测试readyOps()方法的结果的效果相同。例如:

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

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

SelectionKey对象包含的ready集合与最近一次选择器对所注册的通道所作的检查相同。而每个单独的通道的就绪状态会同时改变。

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

nio,详解

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

这两个方法允许您在键上放置一个“附件”,并在后面获取它。这是一种允许您将任意对象与键关联的便捷

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

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

如果选择键的存续时间很长,但您附加的对象不应该存在那么长时间,请记得在完成后清理附件。否则,您附加的对象将不能被垃圾回收,您将会面临内存泄漏问题。

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

等价于:

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

我们已经探讨了SelectionKey的 API,但我们还没有谈完选择键的一切——远远没有。让我们进一步了解如何使用选择器管理键吧。

nio,详解

Java nio入门教程详解(三十四)

Java 2013 年 8 月 22 日 暂无评论

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

4.3.1 选择过程

在详细了解API之前,您需要知道一点和Selector内部工作原理相关的知识。就像上面探讨的那样,选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey对象中的。每一个Selector对象维护三个键的集合:

已注册的键的集合(Registered key set)

与选择器关联的已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys()方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发ng.UnsupportedOperationException。

已选择的键的集合(Selected key set)

已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过

selectedKeys()方法返回(并有可能是空的)。

不要将已选择的键的集合与ready集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。

nio,详解

键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出

ng.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集合将是累积的。比特位只会被设置,不会被清理。

nio,详解

3. 步骤2可能会花费很长时间,特别是所激发的线程处于休眠状态时。与该选择器相关的键可能会

同时被取消。当步骤2结束时,步骤1将重新执行,以完成任意一个在选择进行的过程中,键已

经被取消的通道的注销。

4. select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是已选择的键的集合中

的通道的总数。返回值不是已准备好的通道的总数,而是从上一个select()调用之后进入就

绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然

在已选择的键的集合中,但不会被计入返回值中。返回值可能是0。

使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行

的选择操作冲突的优化。注销通道是一个潜在的代价很高的操作,这可能需要重新分配资源(请记

住,键是与通道相关的,并且可能与它们相关的通道对象之间有复杂的交互)。清理已取消的键,并在选择操作之前和之后立即注销通道,可以消除它们可能正好在选择的过程中执行的潜在棘手

问题。这是另一个兼顾健壮性的折中方案。

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

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

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

有时您会想要限制线程等待通道就绪的时间。这种情况下,可以使用一个接受一个超时参数的

select()方法的重载形式:

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

nio,详解

selectNow()方法执行就绪检查过程,但不阻塞。如果当前没有通道就绪,它将立即返回0。 Java nio入门教程详解(三十五)

Java 2013 年 8 月 22 日 暂无评论

4.3.2 停止选择过程

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

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

调用wakeup()

调用Selector对象的wakeup()方法将使得选择器上的第一个还没有返回的选择操作立

即返回。如果当前没有在进行中的选择,那么下一次对select()方法的一种形式的调用将立

即返回。后续的选择操作将正常进行。在选择操作之间多次调用wakeup()方法与调用它一次

没有什么不同。

有时这种延迟的唤醒行为并不是您想要的。您可能只想唤醒一个睡眠中的线程,而使得后续的选

择继续正常地进行。您可以通过在调用wakeup()方法后调用selectNow()方法来绕过这

个问题。尽管如此,如果您将您的代码构造为合理地关注于返回值和执行选择集合,那么即使下

一个select()方法的调用在没有通道就绪时就立即返回,也应该不会有什么不同。不管怎么

说,您应该为可能发生的事件做好准备。

调用 close()

如果选择器的close()方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,就像

wakeup()方法被调用了一样。与选择器相关的通道将被注销,而键将被取消。

调用 interrupt()

如果睡眠中的线程的interrupt()方法被调用,它的返回状态将被设置。如果被唤醒的线程

之后将试图在通道上执行I/O操作,通道将立即关闭,然后线程将捕捉到一个异常。这是由于在

nio,详解

第三章中已经探讨过的通道的中断语义。使用wakeup()方法将会优雅地将一个在select()方法中睡眠的线程唤醒。如果您想让一个睡眠的线程在直接中断之后继续执行,需要执行一些步

骤来清理中断状态(参见Thread.interrupted()的相关文档)。

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

Java nio入门教程详解(三十六)

Java 2013 年 8 月 22 日 暂无评论

4.3.3 管理选择键

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

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

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

清理一个SelectKey的ready集合的方式是将这个键从已选择的键的集合中移除。选择键的就绪状态只有在选择器对象在选择操作过程中才会修改。处理思想是只有在已选择的键的集合中的键才被认为是包含了合法的就绪信息的。这些信息将在键中长久地存在,直到键从已选择的键的集合中移除,以通知选择器您已经看到并对它进行了处理。如果下一次通道的一些感兴趣的操作发生时,键将被重新设置以反映当时通道的状态并再次被添加到已选择的键的集合中。

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

nio,详解

1. 2. 3. 4. 5. 6. 7. 8. 9.

/* *例 4-1. 使用 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;

10. import java.nio.channels.SelectableChannel; 11. import .Socket; 12. import .ServerSocket; 13. import .InetSocketAddress; 14. import java.util.Iterator; 15. /** 16. * Simple echo-back server which listens for incoming stream connections and 17. * echoes back whatever it reads. A single Selector object is used to listen to 18.

* the server socket (to accept new connections) and all the active socket 19. * channels. 20. * 21. * @author Ron Hitchens (ron@) 22. */ 23. public class SelectSockets { 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. } 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 public void go(String[] argv) throws Exception { int port = PORT_NUMBER; if (argv.length > 0) { // Override default listen port port = Integer.parseInt(argv[0]); } public static void main(String[] argv) throws Exception { new SelectSockets().go(argv); public static int PORT_NUMBER = 1234;

nio,详解

45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. l(); 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. }

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.channe

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);

nio,详解

88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112. 113. 114. 115. 116. 117. 118. 119. 120. 121. 122. 123. 124. 125. 126. 127. 128. 129. 130. 131. } }

// 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. *

nio,详解

例 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()方法来避免破坏迭代器内部的状态。

Java nio入门教程详解(三十七)

Java 2013 年 8 月 23 日 暂无评论

本文来源:https://www.bwwdw.com/article/qfr1.html

Top