天天看点

Java NIO 详解(二)

另一方面,异步 i/o 调用不但不会阻塞,相反,您可以注册对特定 i/o 事件诸如数据可读、新连接到来等等,而在发生这样感兴趣的事件时,系统将会告诉您。

异步 i/o 的一个优势在于,它允许您同时根据大量的输入和输出执行 i/o。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 i/o,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。

有了selector,我们就可以利用一个线程来处理所有的channels。线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。

但是,需要记住,现代的操作系统和cpu在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个cpu有多个内核,不使用多任务可能是在浪费cpu能力。不管怎么说,关于那种设计的讨论应该放在另一篇不同的文章中。在这里,只要知道使用selector能够处理多个通道就足够了。

下面这幅图展示了一个线程处理3个 channel的情况:

Java NIO 详解(二)

异步 i/o 中的核心对象名为 selector。selector 就是您注册对各种 i/o 事件兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

然后,就需要注册channel到selector了。

为了能让channel和selector配合使用,我们需要把channel注册到selector上。通过调用 <code>channel.register()</code>方法来实现注册:

注意,注册的channel 必须设置成异步模式 才可以,,否则异步io就无法工作,这就意味着我们不能把一个filechannel注册到selector,因为filechannel没有异步模式,但是网络编程中的socketchannel是可以的。

需要注意register()方法的第二个参数,它是一个“interest set”,意思是注册的selector对channel中的哪些时间感兴趣,事件类型有四种:

connect

accept

read

write

通道触发了一个事件意思是该事件已经 ready(就绪)。所以,某个channel成功连接到另一个服务器称为 <code>connect ready</code>。一个serversocketchannel准备好接收新连接称为 <code>accept ready</code>,一个有数据可读的通道可以说是 <code>read ready</code>,等待写数据的通道可以说是<code>write ready</code>。

上面这四个事件对应到selectionkey中的四个常量:

如果你对多个事件感兴趣,可以通过or操作符来连接这些常量:

请注意对<code>register()</code>的调用的返回值是一个selectionkey。 selectionkey 代表这个通道在此 selector 上的这个注册。当某个 selector 通知您某个传入事件时,它是通过提供对应于该事件的 selectionkey 来进行的。selectionkey 还可以用于取消通道的注册。selectionkey中包含如下属性:

the interest set

the ready set

the channel

the selector

an attached object (optional)

就像我们在前面讲到的把channel注册到selector来监听感兴趣的事件,interest set就是你要选择的感兴趣的事件的集合。你可以通过selectionkey对象来读写interest set:

通过上面例子可以看到,我们可以通过用and 和selectionkey 中的常量做运算,从selectionkey中找到我们感兴趣的事件。

ready set 是通道已经准备就绪的操作的集合。在一次选selection之后,你应该会首先访问这个ready set。selection将在下一小节进行解释。可以这样访问ready集合:

可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

我们可以通过selectionkey获得selector和注册的channel:

可以将一个对象或者更多信息attach 到selectionkey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的buffer,或是包含聚集数据的某个对象。使用方法如下:

还可以在用register()方法向selector注册channel的时候附加对象。如:

selectionkey key = channel.register(selector, selectionkey.op_read, theobject);

一旦向selector注册了一或多个通道,就可以调用几个重载的<code>select()</code>方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“read ready”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道:

int select(): 阻塞到至少有一个通道在你注册的事件上就绪

int select(long timeout):select()一样,除了最长会阻塞timeout毫秒(参数)

int selectnow(): 不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。

select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道处于就绪状态。

一旦调用了<code>select()</code>方法,它就会返回一个数值,表示一个或多个通道已经就绪,然后你就可以通过调用<code>selector.selectedkeys()</code>方法返回的selectionkey集合来获得就绪的channel。请看演示方法:

当你通过selector注册一个channel时,<code>channel.register()</code>方法会返回一个selectionkey对象,这个对象就代表了你注册的channel。这些对象可以通过<code>selectedkeys()</code>方法获得。你可以通过迭代这些selected key来获得就绪的channel,下面是演示代码:

这个循环遍历selected key的集合中的每个key,并对每个key做测试来判断哪个channel已经就绪。

请注意循环中最后的<code>keyiterator.remove()</code>方法。selector对象并不会从自己的selected key集合中自动移除selectionkey实例。我们需要在处理完一个channel的时候自己去移除。当下一次channel就绪的时候,selector会再次把它添加到selected key集合中。

<code>selectionkey.channel()</code>方法返回的channel需要转换成你具体要处理的类型,比如是serversocketchannel或者socketchannel等等。

某个线程调用select()方法后阻塞了,即使没有通道就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用<code>selector.wakeup()</code>方法即可。阻塞在select()方法上的线程会立马返回。

如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”

当用完selector后调应道掉用<code>close()</code>方法,它将关闭selector并且使注册到该selector上的所有selectionkey实例无效。通道本身并不会关闭。

下面通过一个multiportecho的例子来演示一下上面整个过程。