Selector.wakeup() 提供了使线程从被阻塞的select()方法中优雅的退出的能力
同样有三种方式唤醒select()方法中睡眠的线程:
1 Selector.wakeup() 将使Selector上第一个没有返回的select()操作立即返回,如果当前没有正在进行的select()操作,则下次select()操作会立即返回。
2 Selector.close() 被调用时,所有在select()操作中阻塞的线程都将被唤醒,Selector相关通道被注销,相关SelectionKey将被取消
3 Thread.interrupt()被调用时,返回状态将被设置。如果唤醒之后的线程试图在通道上执行IO操作,通道会立即关闭,线程捕捉到一个异常
如果想让一个睡眠线程在中断之后继续进行,需要执行一些步骤来清理中断状态
这些操作不会改变单个相关通道,中断一个Selector和中断一个通道是不一样的。Selector只会检查通道的状态。
当一个select() 操作时睡眠的线程发生了中断,对于通道状态而言,是没有歧义的
选择器把合理的管理SelectionKey,以确保它们表示的状态不会变得陈旧的任务交给了程序员
当SelectionKey已经不在选择器的Selected key set中时,会发生什么
当通道上至少一个感兴趣的操作就绪时,SelectionKey的ready集合会被清空,并且当前已经就绪的操作会被添加到ready集合里,该SelectionKey随后会被添加到Selected key set中
清理一个SelectionKey的ready集合的方式是将这个key从Selected key set中移除
SelectionKey的就绪状态只有在Selector的select() 操作过程中才会修改,因为只有Selected key set中的SelectionKey才被认为是包含了合法的就绪信息的,这些信息在SelectionKey中长久存在,知道key从Selected key set中移除,以通知Selector你已经看到并对它进行了处理。当下一次通道的感兴趣的操作发生时,key将被重新设置以反映当时通道的状态,并再次被添加到Selected key set中
这种框架提供了非常大的灵活性。
常规做法是先调用select() 操作,再遍历selectKeys()返回的key的集合。在按顺序进行检查每个key的过程中,相关的通道也根据key的就绪集合进行处理。然后key从Selected key set中移除,检查下一个key。完成后通过调用select() 重复循环。
服务器端使用方法
1 创建ServerSocketChannel,注册到Selector中,感兴趣的操作为accept
2 轮询Selector的select()操作,从就绪key集合中遍历key的ready集合,有accept则调用ServerSocketChannel的accept()方法获取SocketChannel,
其中包含接收到的socket的句柄。再将SocketChannel注册到Selector中感兴趣的操作为read
3 当下次select()操作中key的ready集合中有read时,开始做事
Selector是线程安全的,但key set不是。Selector.keys 和 Selector.selectkeys() 返回的是Selector内部私有key set的引用。这个集合可能随时被改变。迭代器Iterator是快速失败的(fail-fast),如果迭代的时候key set发生改变,抛出ConcurrentModificationException。所有如果希望在多个线程间共享Selector或SelectionKey,则要对此做好准备。当你修改一个key的时候,可能会破坏另一个线程的Iterator
如果在多个线程并发的访问一个Selector的key set时,需要合理地同步访问。在select()操作时,先在Selector上进行同步,再是Registered key set,最后是Selected key set。按照这样的顺序,Cancelled key set就会在第一步和第三步之间保持同步。
在多线程的场景中,如果需要对任何一个key set进行更改,不管是直接更改还是其他操作带来的副作用,都需要以相同的顺序,在同一对象上进行同步。如果竞争的线程没有以同样的顺序请求锁,则会有死锁的隐患。
Selector的close()和select()操作都会有一直阻塞的可能,当在select()的过程当中,所有对close()的调用都会被阻塞,直到select()结束,或者执行select()的线程进入睡眠。当select()的线程进入睡眠时,close()的线程获得锁后会立即唤醒select()的线程,并关闭Selector
如果不采取相同的顺序同步,则key set中key的信息不保证是有效的,相关通道也不保证是打开的
一个cpu的时候使用一个线程来管理所有通道,是一个合适的解决方案,但会浪费其他cpu的运行能力
在大量通道上执行select()操作不会有太大开销,因为大多数工作都是操纵系统完成的
方案1 管理多个Selector,并随机分配通道不是一个好方案
方案2 所有通道使用一个Selector,将就绪通道的服务委托给其他线程。相当于用一个线程监控通道的就绪状态,使用另外的工作线程池来处理接收到的数据。而线程池是可以调整的,或者动态调整。
在方案2中,如果某些通道要求更高的响应速度,可以用两个选择器来解决。并且线程池可以细化为日志线程池、命令线程池、状态请求线程池等