Java NIO——3 非阻塞与selector

好吧,先看一下通道选择的三个关键的类Selector、SelectionKey、SocketChannel、channel的关系
Java NIO——3 非阻塞与selector_第1张图片
 
一、选择器基础
1、选择器提供选择执行已经就绪的任务的能力,这使得多元 I/O 成为可能。


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


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

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


4、选择器,可选择通道和选择键类
(1)选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。

(2)一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。 

(3)选择键selectionkey封装了特定的通道与特定的选择器的注册关系。  由选择键对象被SelectableChannel.register( ) 返回

(4)通道在被注册到一个选择器上之前,必须先设置为非阻塞模式(通过调用configureBlocking(false))。通道一旦被注册,就不能回到阻塞状态。
通道创建默认都是非阻塞编
socketchannel.configureBolcking(false);
socketchannel.register(selector, [注册类型]);

(5)一个给定的通道可以被注册到多于一个的选择器上,而且不需要知道它被注册了哪个 Selector 对象上。

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


5、建立选择器
(1)需要调用close( )方法来释放它可能占用的资源并将所有相关的选择键设置为无效。

(2)选择器包含了注册到它们之上的通道的集合。在任意给定的时间里,对于一个给定的选择器和一个给定的通道而言,只有一种注册关系是有效的。

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

(4)isRegistered()方法在一个键被取消之后,直到通道被注销为止,可能有时间上的延迟。这个方法只是一个提示,而不是确切的答案。



二、使用选择键
(1)当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( )方法时(或者一个正在进行的 select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。

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

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

(4)ready 集合(readyOps : int)是 interest集合的子集,并且表示了 interest 集合中从上次调用 select( )以来已经就绪的那些操作。

(5)通过相关的选择键的readyOps( )方法返回的就绪状态指示只是一个提示,不是保证。

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


// 重新注册感兴趣的OP_READ事件
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
//取消对OP_READ事件的注册
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));



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

Java NIO——3 非阻塞与selector_第2张图片

select 在selector中的处理过程,之后
Java NIO——3 非阻塞与selector_第3张图片

1)已取消的键的集合(canceledKeys)将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的

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

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

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

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

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

(3)select 操作返回的值是 ready 集合在步骤 2 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个 select( )调用之后进入就绪状态的通道的数量。

(4)就绪选择selectNow()是完全非阻塞的。如果当前没有通道就绪,它将立即返回0


2、停止选择过程
(1)有三种方式可以唤醒在select( )方法中睡眠的线程:
1)wakeup()
2)close()
3)interrupt()

如果睡眠中的线程的 interrupt( )方法被调用,它的返回状态将被设置。如果被唤醒的线程之后将试图在通道上执行 I/O 操作,通道将立即关闭,然后线程将捕捉到一个异常。
请注意这些方法中的任意一个都不会关闭任何一个相关的通道。中断一个选择器与中断一个通道是不一样的

3、管理选择键
(1)选择是累积的(selectKeys是积累的)一旦一个选择器将一个键添加到它的已选择的键的集合中,它就不会移除这个键。清理选择器中已选择集合中已处理操作的键交给了程序员
while(true)
{
selector.select();
Set keys=selector.selectedKeys();
Iterator it=keys.iterator();
 
while(it.hasNext()){
    SelectionKey key=it.next();
    it.remove();
    
    //处理key
}


(2)当通道上的至少一个感兴趣的操作就绪时,键的 ready 集合就会被清空,并且当前已经就绪的操作将会被添加到ready集合中。该键之后将被添加到已选择的键的集合中。 

(3)清理一个 SelectedKey 的 ready 集合的方式是将这个键从selector已选择的键的集合(selectedKeys)中移除。选择键的就绪状态只有在选择器对象在选择操作过程中才会修改。

(4)通常的做法是在选择器上调用一次 select 操作(这将更新已选择的键的集合),然后遍历 selectKeys( )方法返回的键的集合。在按顺序进行检查每个键的过程中,相关的通道也根据键的就绪集合进行处理。然后键将从已选择的键的集合中被移除(通过在 Iterator对象上调用 remove(  )方法),然后检查下一个键。完成后,通过再次调用 select( )方法重复这个循环。

4、并发性
(1)选择器对象是线程安全的,但它们包含的键集合不是。通过 keys( )和 selectKeys( )返回的键的集合是 Selector 对象内部的私有的 Set 对象集合的直接引用。这些集合可能在任意时间被改变。已注册的键的集合是只读的。如果您试图修改它,那么您得到的奖品将是一个java.lang.UnsupportedOperationException,但是当您在观察它们的时候,它们可能发生了改变的话,Iterator 对象是快速失败的(fail-fast):如果底层的 Set 被改变了,它们将会抛出 java.util.ConcurrentModificationException,因此如果您期望在多个线程间共享选择器和/或键,请对此做好准备。您可以直接修改选择键,但请注意您这么做时可能会彻底破坏另一个线程的Iterator。
 
(2)如果在多个线程并发地访问一个选择器的键的集合的时候存在任何问题,您可以采取一些步骤来合理地同步访问。在执行选择操作时,选择器在 Selector 对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合,按照这样的顺序。 

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

(4)Selector 类的 close( )方法与 select( )方法的同步方式是一样的,因此也有一直阻塞的可能性。在选择过程还在进行的过程中,所有对 close( )的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。

(5) 使用 select( )来为多个通道提供服务(例子selector.SelectSockets)

四、异步关闭能力
1、一个特定的键的集合中的一个键的存在并不保证键仍然是有效的,或者它相关的通道仍然是打开的。 

2、当一个通道关闭时,它相关的键也就都被取消了。这并不会影响正在进行的select( ),但这意味着在您调用select( )之前仍然是有效的键,在返回时可能会变为无效。

五、选择过程的可扩展性
1、不同通道请求不同的服务类的办法:
1)在第一个场景中,如果您想要将更多的线程来为通道提供服务,一个更好的策略是对所有的可选择通道使用一个选择器,并将对就绪通道的服务委托给其他线程。您只用一个线程监控通道的就绪状态并使用一个协调好的工作线程池来处理共接收到的数据。(阻塞编程)
这就是以前处理客户端和服务器端的方式了,不过线程的开销太大了,这种基于阻塞的编程并不能很好的利用到系统的资源


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

你可能感兴趣的:(Java,NIO,Java)