唠唠~NIO中的Selector选择器

在前面的博客中介绍了NIO中的Buffer、Channel、Selector以及NIO模型的简单演示。我认为在NIO中最难理解的、东西比较多的当属于Selector选择器莫属,今天想再说说Selector选择器。

Selector的优点:可以使用更少线程来管理channel(线程会占用内存资源,线程的切换对系统的开销也很大,一般线程越少越好,但随着现在机器性能提高,多核计算使用单线程反而浪费资源,),理论上可以由一个线程管理所有channel。

Selector本身为抽象类,AbstractSelector为Selector类的抽象实现类。Selector即为“选择器”,支撑了NIO的多路复用。Selector不可以直接创建,需要借助Selector.open()方法来进行创建,最后可以通过选择器的close方法关闭它。

通常一个Channel可以被注册到一个Selector上,而Selector可以检测多个Channel,事实上,Channel可以注册到任意一个Selector上,ServerSocketChannel和SocketChannel可以共用一个Selector,也可以各自使用不同的选择器。

SelectorKey介绍

一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。

取出SelectionKey所关联的Selector(选择器对象)和Channel (通道对象)

      Channel channel =selectionKey.channel();                           Selector selector=selectionKey.selector();

通过SelectionKey对象的cancel()方法可以取消特定的注册关系。该方法调用之后,该SelectionKey对象将会被”拷贝”至已取消键的集合中,该键此时已经失效,但是该注册关系并不会立刻终结。在下一次select()时,已取消键的集合中的元素会被清除,相应的注册关系也真正终结。

为SelectionKey绑定附加对象

可以将一个或者多个附加对象绑定到SelectionKey上,以便容易的识别给定的通道。通常有两种方式: 
在注册的时候直接绑定:SelectionKey key=channel.register(selector,SelectionKey.OP_READ,theObject);

在绑定完成之后附加:selectionKey.attach(theObject);//绑定

绑定之后,可通过对应的SelectionKey取出该对象:selectionKey.attachment();。

如果要取消该对象,则可以通过该种方式:selectionKey.attach(null).

需要注意的是如果附加的对象不再使用,一定要人为清除,因为垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。

一个单独的通道可被注册到多个选择器中,有些时候我们需要通过isRegistered()方法来检查一个通道是否已经被注册到任何一个选择器上。

通过Selector选择通道

选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中。Selector维护的三种类型SelectionKey集合如下:

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

所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys()方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的。

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

已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(并有可能是空的)。 每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。

已取消的键的集合(Cancelled key set)

已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

在刚初始化的Selector对象中,这三个集合都是空的。通过Selector的select()方法可以选择已经准备就绪的通道(这些通道包含你感兴趣的的事件)。

下面是Selector几个重载的select()方法:

select():阻塞到至少有一个通道在你注册的事件上就绪了。 
select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒。 
selectNow():非阻塞,只要有通道就绪就立刻返回。

select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。

一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合。如下: 
Set selectedKeys=selector.selectedKeys(); 进而可以放到和某SelectionKey关联的Selector和Channel。


关于Selector执行选择的过程【select()执行过程。】

1、首先检查已取消键集合,也就是通过cancle()取消的键。如果该集合不为空,则清空该集合里的键,同时该集合中每个取消的键也将从已注册键集合和已选择键集合中移除。(一个键被取消时,并不会立刻从集合中移除,而是将该键“拷贝”至已取消键集合中,这种取消策略就是我们常提到的“延迟取消”。)

2、再次检查已注册键集合(准确说是该集合中每个键的interest集合)。系统底层会依次询问每个已经注册的通道是否准备好选择器所感兴趣的某种操作,一旦发现某个通道已经就绪了,则会首先判断该通道是否已经存在在已选择键集合当中,如果已经存在,则更新该通道在已注册键集合中对应的键的ready集合,如果不存在,则首先清空该通道的对应的键的ready集合,然后重设ready集合,最后将该键存至已注册键集合中。这里需要明白,当更新ready集合时,在上次select()中已经就绪的操作不会被删除,也就是ready集合中的元素是累积的,比如在第一次的selector对某个通道的read和write操作感兴趣,在第一次执行select()时,该通道的read操作就绪,此时该通道对应的键中的ready集合存有read元素,在第二次执行select()时,该通道的write操作也就绪了,此时该通道对应的ready集合中将同时有read和write元素。

 停止选择的方法

选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。

1、通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回 
该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
2、通过close()方法关闭Selector
该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。
3、调用interrupt() 
调用该方法会使睡眠的线程抛出InterruptException异常,捕获该异常并在调用wakeup()

并发性​​​​​​​


Selector在多线程环境使用是线程安全的,但是他们的选择键集合并不是。通过keys()和selectKeys()方法返回的选择键集合是Selector对象内部私有的Set对象集合的引用。已注册的选择键集合应该是只读的,如果试图去修改它,将会抛出UnsupportededOperationException。在Selection过程中,Selector对象自身进行同步,然后是已注册的选择键集合、已选择的选择键集合,按照这样的顺序,已取消的键的集合在选择的过程的第一步和第三步保持同步(当与取消的选择键的集合相关的通道被注销时)。在Selection过程中对感兴趣的集合的修改不会被当前调用过程产生影响,他们会在下一次的selection过程生效。在任何时候,都可以取消选择键跟关闭通道。因此在选择键集合的键并不意味着选择键是合法的或者通道还在打开。
因此,我们需要注意同步跟检查是否存在其他线程进行取消键跟关闭通道的可能。因此,除非使用同步,否则键的状态和相关的通道在任何时候都会发生改变,一个特定的键的集合的一个键并不能保存是有效的或者通道仍然打开。如果使用一个已经失效的键,将会抛出CancelledKeyException,如果使用已经关闭的通道,将会导致ClosedChannelException。通道提供了异步关闭的能力,当一个通道关闭的时候,它相关的键都会被取消。不过不会影响正在进行的selection操作,防止出现一个线程在关闭一个正在进行Selection操作的通道的时候,被阻塞与无限的等待的可能。同样,我们应该通过selectkeys方法来获取已经选择的键的集合,而不是自己主动去维护选择键的集合。

在多线程并发的环境共享SelectionKey或者选择键集合,因为iterator是fail-fast的,如果在迭代过程中,Set对象的结构发生了变化,将会抛出concurrentModificationException异常。

猜你喜欢

转载自blog.csdn.net/qq_40303781/article/details/88661159