通过上一篇对JAVA NIO的源码分析,对一些重要的代码实现进行了探究,现将从源码分析中得出的结论总结如下。
一、源码分析流程梳理。
1.Selector.open() 获取选择器的时候,根据不同的操作系统创建Selector实现类,实现类创建了用于保存通道句柄和事件类型的数据结构PollArrayWrapper,如果是Windows系统将会创建一对相互连接的socket通道模拟管道用于唤醒,而Linux对于内核版本>=2.6并且JDK>1.5u9的情况下可以创建更加高效的epoll模式的Selector实现类。并且Linux系统可以直接利用操作系统的管道做到唤醒的功能。
2.ServerSocketChannel.register(...)通道注册,根据channel和selector创建了把两者关联起来的SelectionKeyImpl对象,并将其记录到已注册键的集合中,同时将socket句柄以及事件添加至PollArrayWrapper结构中。另外针对操作系统对最大句柄数的限制可能还需要创建更多的线程。
3.Selector.select();在执行选择操作之前,首先对已经取消的键集合进行清理,并调整线程数(因最大句柄数限制而创建的helper线程),然后调用操作系统底层的select函数,把检查通道是否就绪的工作移交至操作系统,如果是Windows则是通过轮询的方式进行检测,如果是Linux则可能是select/poll也是轮询或者基于中断的更高效的epoll的方式进行检测,这取决于jdk的版本和内核的版本。当系统底层的select调用返回之后,再次进行对已经取消的键集合进行清理,并返回继上一次select操作之后到本次结束新就绪的通道数量。
4.Selector.wakeup(),通过向管道(Windows是互联的socket通道)的sink端写入一个字节来唤醒阻塞在select的调用,并且连续多次的唤醒动作将等同于一次调用。
二、相关知识点
1.首先Selector.open()并不是单例模式,当你每次调用该静态方法时候,都返回一个全新的Selector实例。
2.Selector能够通过调用configureBlocking来设置是否启用非阻塞模式。其默认为阻塞模式。
3.服务端和客户端是否维护着同一份Selector,答案是否定的,服务端和客户端各自维护着一个Selector对象,并且注意在多线程并发的时候,虽然Selector是线程安全的,但是其内部的重要成员集合(registeredKeys、selectedKeys、cancelledKeys)是非线程安全的。如果在多个线程并发地访问一个选择器的键的集合的时候存在任何问题,您可以采取一些步骤来合理地同步访问。
4.在执行选择操作时,选择器在 Selector 对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合,按照这样的顺序。 在多线程的场景中,如果您需要对任何一个键的集合进行更改,不管是直接更改还是其他操作带来的副作用,您都需要首先以相同的顺序,在同一对象上进行同步。锁的过程是非常重要的。如果竞争的线程没有以相同的顺序请求锁,就将会有死锁的潜在隐患。如果您可以确保否其他线程不会同时访问选择器,那么就不必要进行同步了。
5.channel.read()函数会返回-1,那么什么时候会读到-1呢?针对服务器端而言,当客户端调用了channel.close()关闭连接时,这时候服务器端返回的读取数是-1,表示已经到了末尾。那么此时需要把对应的SelectionKey给cancel掉,表示selector不再监听这个channel上的读事件,并且关闭channel。
6.虽然说一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
7.当通道关闭时,所有相关的键会自动取消;当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化。
8.注意select()
操作返回值不是已经准备好的通道的总数,而是从上一个select()
调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中,返回值可能是0,这也是为何返回0时,需要continue的原因。
9.Selector 类的 close( )方法与 select( )方法的同步方式是一样的,因此也有一直阻塞的可能性。在选择过程还在进行的过程中,所有对 close( )的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。
10.当一个通道关闭时,它相关的键也就都被取消了。这并不会影响正在进行的select( ),但这意味着在您调用select( )之前仍然是有效的键,在返回时可能会变为无效。
三、SelectionKey
selectionKey表示了通道(channel)与选择器(selector)之间的注册关系,以及维护了通道的事件。
SelectionKey 包含了两个集合(其实是以整数形式进行编码的byte掩码):
1、 注册的感兴趣的操作集合 即:interestOps集合。
2、已经准备好的操作集合(就绪集合) 即:readyOps集合。
Selector维护了三个集合:
1、已经注册的键集合 调用, keys()
2、已经选择的键集合 调用, selectedKeys()
3、已经取消的键集合 私有, cancelledKeys
这些集合之间的流转关系:
1. 当调用cancel操作的时候,只是把要取消的键加入到了cancelledKeys键集合中,需要等到下次调用select的时候进行才会生效。但是SelectionKey的isValid()会立即回复false。
2. 在操作系统返回就绪操作的通道的时候:
a)如果通道的selectionkey还没有在已经选择的键的集合(selectedKeys)中,那么键的readyOps集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。
b)否则,一旦通道的键被放入已经选择的键的集合中时,ready集合不会被清除,而是累积。这就是说,如果之前的状态是ready的操作,本次已经不是ready了,但是他的bit位依然表示是ready,不会被清除。
从2可以看出我们为什么每次循环selectedKeys的时候都需要调用it.remove().
另外:一个channel中的数据没读完(或有数据而不处理),那么,这个channel一直处于就绪状态中,所以每次selector的selectedKeys()方法总能返回与这个channel关联的Selectionkey,然后就会不停地循环select(),除非读完channel中的数据,或者把这个SelectionKey给cancel掉。
二、中断select()的方法
select()方法会阻塞住,等待有channel就绪才返回。有时候,希望停止阻塞,中断select方法,让线程继续。
有三种方法。
1, wakeup()这是一种优雅的方法,立即返回阻塞在select的线程。如果当前没有阻塞在select上,则本次wakeup调用将作用在下一次select操作上,也就是下一次select调用将立即返回无论是否有就绪发生。
2, close()选择器的close被调用,则所有在选择操作中阻塞的线程被唤醒,相关通道被注销,键也被取消。
3, interrupt() 实际上interrupt并不会中断线程。而是设置线程中断标志。
然后依然是调用wakeup()。这是因为 Selector 捕获了interruptedException,然后在异常处理中调用了 wakeup()