Java NIO中的Selector和IO复用

快要到农历新年了,但本小站的文章还有一些“挖过的坑”没有填完。之前由于其他因,Java的文章一直没有跟进更新,处于HoldOn状态。这篇文章将继续IO相关的内容,前面已经介绍过Java NIO中Buffer和Channel的概念,本篇进一步整理一下Selector和IO复用。

前面讲到JDK1.4中NIO中的Buffer和Channel通过缓冲机制已经在提高IO效率上做了很多优化,那么把IO复用和这些结合起来,将进一步优化对IO相关程序的设计,提高IO上的使用效率。

0. Selection的概念和意义

在《Java NIO》一书里介绍readiness selection概念是举了一个很好的例子,结合我们今天去银行办理个人业务的情况,我描述讲解一下。虽然我们现在的网银和其它互联网工具已经很发达了,但免不了我们需要去工行、招行办个业务啥的。因为银行网点多、服务网点门店小,而需要办理业务的人又很多,就不得不排队等待,那么常见的有两种方式,比如3个服务窗口:

  • 可以3个窗口分别排队,为了方便队伍秩序维护,站到某一队的人不得到其它队伍插队,相互分隔开来,每一个队伍单独维护着先后顺序
  • 另一种方式是当你走进招商银行正门,服务员给你打一张号码条,即不分窗口进行全局排队,无论服务窗口有几个都是公用的,只要全局排队号码在前面的人已经完成了服务,则下一个号码的人就可以去空闲窗口去办理业务

这个描述大概说了下不使用和使用readiness selection的情况,可能在效率体现上也未必完全恰当,但在计算机编程上,readiness selection和IO复用在特定场景下很大的提高了效率。我之前整理过Java并发的文章,有一个观点就是,并发并不是线程越多越好,一方面这需要很大的维护成本,更重要的是我们的计算机处理资源都是非常有限的,多开一个线程就多耗费一些资源,所以我们可能会考虑使用一个线程熟练有上限的线程池。那么如果每个线程负责处理一个网络连接,线程占用达到上限的时候,新的连接又将如何处理?已被之前连接占用的线程始终不被释放,这样调度是否是最高效的?在Java中,Selector和IO复用很好的解决了这个问题。

其实Java中的Selector和IO复用是基于各个操作系统平台实现的。在操作系统底层的API中,也早有select的概念,有select()、poll()等函数。

随着Ajax技术和Web长连接推送应用场景的发展,NIO和IO复用有了很大的需求。在Java开源项目的Jetty和Tomcat服务器实现中,也都对NIO的IO复用机制做了很大支持。

在readiness selection的设计和实现中,有三个重要角色SelectableChannel、SelectionKey和Selecotor。

SelectableChannel、SelectionKey和Selector之间的关系

Java中readiness selection中SelectableChannel、SelectionKey和Selector之间的关系

1. SelectableChannel

在之前的Java文章中,我已经将Buffer和Channel的概念整理介绍出来了,但关于Channel的分类和具体使用细节,暂时就不准备过度赘述了,这里说下SelectableChannel。在Java第4版的API中,Channel的子类分为两大块:

  • FileChannel,针对文件IO的Channel,可以通过FileInputStream、FileOutputStream和RandomAccessFile来获得,不支持非阻塞模式,进而也就不支持readiness selection
  • SelectableChannel,除File以外,像对Socket IO做支持的Channel都属于SelectableChannel,支持非阻塞模式和readiness selection

我们这里讲的readiness selection就需要SelectableChannel在非阻塞模式下使用,可以通过

public abstract void configureBlocking (boolean block)
throws IOException;

这个方法进行配置。从上面的关系图中我们可以看到Channel最终是要和Selector关联起来使用的,实际上是通过SelectableChannel中的register()方法进行注册的。

public abstract SelectionKey register (Selector sel, int ops)
throws ClosedChannelException;
public abstract SelectionKey register (Selector sel, int ops,
Object att)
throws ClosedChannelException;

第二个参数是感兴趣的事件,默认常量有4个(连接、接受、读、写),定义在SelectionKey类中,但并不是所有Channel都一定支持,可以用validOps()判断。除此之外,同一SelectableChannel对象可以注册到多个Selector,可以调用它的keyFor()方法,来得到对应的SelectionKey。

2. SelectionKey

接下来说说在Channel和Selector之间的关联对象SelectionKey。既然是关联对象,那肯定是可以得到连接的两个对象的:

public abstract SelectableChannel channel()
public abstract Selector selector()

还有支持的感兴趣的事件,以及已经准备好IO的事件,感兴趣的事件的方法是同名重载,一个为get另一个为set:

public abstract int interestOps( );
public abstract void interestOps (int ops);
public abstract int readyOps( );

Selection中维护了两个Set集合,正如上面方法中所示,一个是感兴趣的事件集合,另一个是准备好了的,可以进行IO操作的集合。

对于SelectionKey的cancel()方法需要注意的是,并不直接生效,而是到Selector下次select()时,但SelectionKey的isValid()会立即回复false。

3. Selector

终于,最重要的对象出现了。通常,Selector是由静态工厂方法open()实例化的,也可以直接调用SelectorProvider的openSelector()返回,Selector的provider()方法会返回特定的provider对象。用完了调用close()以释放资源,可以用isOpen()判断Selector是否已经关闭。

public abstract int select( ) throws IOException;
public abstract int select (long timeout) throws IOException;
public abstract int selectNow( ) throws IOException;
public abstract void wakeup( );

当Selector和特定的SelectableChannel关联好了,开始工作了,那么就需要进行select操作,如上面方法所示。

  • select() 阻塞调用线程,直到有某个Channel的某个感兴趣的Op准备好了
  • select(long) 阻塞调用线程,但超时会自动返回
  • selectNow() 则不阻塞
  • wakeup() 则是从另一线程对Selector调用,恢复调用select()的线程执行;注意这这是取消最近一次的调用,如果还没有调用,则下一次调用会直接返回

select()只返回本次执行select时从未准备好到准备好状态的channel数,如果不为0,将调用如下方法进行处理。

public abstract Set selectedKeys( );

这个方法返回一个包含SelectionKey对象的集合,分别对应各个准备好的Channel。而对于注册在这个Selector的所有Key,还有一个方法可以获取到。

public abstract Set keys( );

Selector对象维护了3个key集合,一个注册过的,一个是选择过的,最后一个是cancel过但是未反注册的,这个我们没有方法直接获取到。

4. 常规使用示例

了解过了这3个重要角色,看一段常规使用的代码示例。

		int port = 30;
		ServerSocketChannel serverChannel = ServerSocketChannel.open( );
		ServerSocket serverSocket = serverChannel.socket( );
		Selector selector = Selector.open( );
		serverSocket.bind (new InetSocketAddress (port));
		serverChannel.configureBlocking (false);
		serverChannel.register (selector, SelectionKey.OP_ACCEPT);
		while (true) {
			int n = selector.select( );
			if (n == 0) {
				continue; // nothing to do
			}
			Iterator it = selector.selectedKeys().iterator( );
			while (it.hasNext( )) {
				SelectionKey key = (SelectionKey) it.next( );
				if (key.isAcceptable( )) {
					ServerSocketChannel server =
					(ServerSocketChannel) key.channel( );
					SocketChannel channel = server.accept( );
					if (channel == null) {
						;//handle code, could happen
					}
					channel.configureBlocking (false);
					channel.register (selector, SelectionKey.OP_READ);SelectionKey.OP_READ);

				}
				if (key.isReadable( )) {
					readDataFromSocket (key);
				}
				it.remove( );
			}
		}

这是一个简单服务器接受请求,并做读取的代码逻辑。

readDataFromSocket (key);

这句是通过Channel和Buffer进行数据读取处理。
注意最后的:

it.remove( );

这行代码是必要的。

5. ReadinessSelection注意点和IO复用

为了解释为什么上面实例中最后的iterator的remove()调用是必要的,我们需要先来看下Java在select实现上的原理和过程。

首先,针对关联每个Channel的SelectionKey对象,都维护者2个Set集合,分别是

  • interestOps
  • readyOps

然后,每个Selector又维护着3个Set集合,分别是

  • registeredKeys,可以通过keys()方法获得
  • selectedKeys
  • cancelledKeys,存储着调用过cancel()方法,但并没有被反注册或者说解开注册的SelectionKey对象,没有方法直接获得

每次select()方法调用时,先把cancelledKeys数据同步到registerKeys和selectedKeys,做减法以完成反注册,接下来调用操作系统底层的select实现,重点在于阻塞之后得到的结果处理:

  • 如果有在registeredKeys中的key的感兴趣事件发生了,检查是否该key存在于selectedKeys中,如果没有,则将该key的readOps清空,根据此次的情况进行重新设置,并将key加入到selectedKeys
  • 如果不是上面这种情况,即selectedKeys中已经包含了事件中的key,那么只做“从无到有”的更新操作,这里的所谓“从无到有”就是如果原来已经有了的key不做自动移除,key对应的readOps也只是将之前没有ready而此次ready的放进去,不会将之前ready而此时已经非ready的做更新

说道这里,remove()的必要性就不必多解释了,在select()返回之前,再将阻塞过程当中发生cancel的key做一次同步。

上面提到了几个集合,其实Selector对象本身的操作是线程安全的,但3个keySet是可能随时变化的,可以获取到再进行更改,这个Set的使用需要额外做同步来保证线程安全。

其实,大多数情况下使用Selector的select()只需单线程就可以满足了,而对于select得到的channel和对应的IO操作,可以新开线程或者使用线程池来处理。这也正是IO复用的意义所在。

关于Java NIO中Selector的使用,本文件就解释到这里。更简洁的NIO使用介绍可以参看这里:

http://ifeve.com/java-nio-all/

关于操作系统的select()、poll()和epoll可以参看:

http://www.cnblogs.com/bigwangdi/p/3182958.html

http://blog.csdn.net/tianmohust/article/details/6677985

相关文章:

此条目发表在 IO, Java, Java语言, 开发, 计算机技术 分类目录,贴了 , , , , , 标签。将固定链接加入收藏夹。

Java NIO中的Selector和IO复用》有 10 条评论

  1. 李幼群 说:

    说的很好,我看书没看太明白,这里说的很好,终于算是领悟写了,很感谢博主啊 ^_^

  2. SWEAR 说:

    这个例子举得非常好,一目了然,毛塞顿开

  3. 六哥 说:

    remove 有什么作用

  4. 蒲公英 说:

    博主,select方法阻塞的意思是一直等到有注册时间响应吗?那这样的话select方法有返回0的可能吗?求博主解答⊙﹏⊙

  5. 蒲公英 说:

    博主,select方法阻塞的意思是一直等到有注册时间响应吗?那这样的话select方法有返回0的可能吗?求博主解答⊙﹏⊙

  6. 蜡笔小新 说:

    感觉nio为什么设计得这么难用。

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>