NIO选择器学习笔记

选择器的作用

     《Java NIO》上面的例子感觉有点晦涩,个人觉得Selector就像一条传送带一样,很多商品(Channel)通过传送带传过来,在另一端很多人(多个线程)在分拣处理。

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

 

基础

     在使用时,调用SelectableChannel.register方法来把当前通道注册到一个Selector上,并且返回一个SelectionKey。在把通道注册到选择器之前,必需得把通道设置为非阻塞模式,否则会抛异常的。

     SelectableChannel是一个抽象类,是SocketChannel,ServerSocketChannel以及DatagramChannel等通道的父类;Selector自然就是选择器了;SelectionKey定义了四种操作:读、写、连接(connect)和接受(accept)。并不是所有的操作都一定被支持,比如SocketChannel不支持accept,注册了不支持的操作就会抛异常。任何一个通道和选择器的注册关系都被封装到一个SelectionKey中。

     在任意时间里,一个给定的选择器和一个给定的通道之间只能存在一种注册关系,比如先注册为读,然后注册为读写,那么关系就是读写,以最后的为准;另外虽然只能存在一种注册关系,但是一个通道可以注册到多个选择器上,这样就相当于与每个选择器都保持一种注册关系。

 

有关选择键

      SelectionKey表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用 SelectionKey 对象的 cancel() 方法。可以通过调用 isValid() 方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。

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

      一个 SelectionKey 对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(instrest 集合),另一个表示通道准备好要执行的操作(ready 集合)。

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

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

     这四个方法在任意一个 SelectionKey 对象上都能安全地调用。不能在一个通道上注册一个它不支持的操作,这种操作也永远不会出现在 ready 集合中。调用一个不支持的操作将总是返回 false,因为这种操作在该通道上永远不会准备好。
      通过相关的选择键的 readyOps() 方法返回的就绪状态指示只是一个提示,不是保证。底层的通道在任何时候都会不断改变。其他线程可能在通道上执行操作并影响它的就绪状态。同时,操作系统的特点也总是需要考虑的。

     在注册选择器的时候,还可以为它加个对象(attach那个参数),这个对象可以引用任何对您而言有意义的对象,SelectionKey只管存,不会拿着它做别的。

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() 方法的调用会阻塞不确定长的一段时间。选择器所使用的锁策略(例如是否在整个选择过程中保持这些锁)是依赖于具体实现的。幸好,这种多元处理能力被特别地设计为可以使用单线程来管理多个通道。被多个线程使用的选择器也只会在系统特别复杂时产生问题。

 

选择器的使用

     每一个 Selector 对象维护三个键的集合:已注册的键的集合(Registered key set),已选择的键的集合(Selected key set),已取消的键的集合(Cancelled key set)

     选择器是对 select()、poll() 等本地调用(native call)或者类似的操作系统特定的系统调用的一个包装。但是 Selector 所作的不仅仅是简单地向本地代码传送参数。它对每个选择操作应用了特定的过程。对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。

 

选择器的执行过程(这段不太懂,内容先放这里,过几天再回顾一下的)

选择操作是当三种形式的 select() 中的任意一种被调用时,由选择器执行的。不管是哪一种形式的调用,下面步骤将被执行:


     1. 已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。
     2. 已注册的键的集合中的键的 interest 集合将被检查。在这个步骤中的检查执行过后,对 interest 集合的改动不会影响剩余的检查过程。
     一旦就绪条件被定下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态。依赖于特定的 select() 方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。对于那些还没准备好的通道将不会执行任何的操作。对于那些操作系统指示至少已经准备好 interest 集合中的一种操作的通道,将执行以下两种操作中的一种:

     如果通道的键还没有处于已选择的键的集合中,那么键的 ready 集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。

     否则,也就是键在已选择的键的集合中。键的 ready 集合将被表示操作系统发现的当前已经准备好的操作的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的 ready 集合是与之前的 ready 集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的 ready 集合将是累积的。比特位只会被设置,不会被清理。假设之前的 ready 集合为 100,此次 010 的操作已就绪,此时的 ready 集合为 110,而不是 010。这就是累积,不会被清理。

3. 步骤 2 可能会花费很长时间,特别是所激发的线程处于休眠状态时。与该选择器相关的键可能会同时被取消。当步骤 2 结束时,步骤 1 将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注销。
     4. select 操作返回的值是 ready 集合在步骤 2 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个 select() 调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是 0。

 

停止选择的过程

调用 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() 方法重复这个循环。

 

选择器的并发

     选择器对象是线程安全的,但它们包含的键集合不是。通过 keys() 和selectKeys()返回的键的集合是 Selector 对象内部的私有的 Set 对象集合的直接引用。这些集合可能在任意时间被改变。如果期望在多个线程间共享选择器或键,需要对此做好准备。您可以直接修改选择键,但请注意您这么做时可能会彻底破坏另一个线程的 Iterator。

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

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

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

 

异步关闭能力

     任何时候都有可能关闭一个通道或者取消一个选择键。除非您采取步骤进行同步,否则键的状态及相关的通道将发生意料之外的改变。一个特定的键的集合中的一个键的存在并不保证键仍然是有效的,或者它相关的通道仍然是打开的。 

     关闭通道的过程不应该是一个耗时的操作。NIO 的设计者们特别想要阻止这样的可能性:一个线程在关闭一个处于选择操作中的通道时,被阻塞于无限期的等待。当一个通道关闭时,它相关的键也就都被取消了。这并不会影响正在进行的 select( ),但这意味着在您调用 select( )之前仍然是有效的键,在返回时可能会变为无效。您总是可以使用由选择器的 selectKeys( ) 方法返回的已选择的键的集合。不要自己维护键的集合。理解选择过程,对于避免遇到问题而言是非常重要的。

 

选择过程的可扩展性

     选择器可以简化用单线程同时管理多个可选择通道的实现。使用一个线程来为多个通道提供服务,通过消除管理各个线程的额外开销,可能会降低复杂性并可能大幅提升性能。

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

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

你可能感兴趣的:(学习笔记)