快要到农历新年了,但本小站的文章还有一些“挖过的坑”没有填完。之前由于其他因,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。
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在非阻塞模式下使用,可以通过
1
2
|
public
abstract
void
configureBlocking (
boolean
block)
throws
IOException;
|
这个方法进行配置。从上面的关系图中我们可以看到Channel最终是要和Selector关联起来使用的,实际上是通过SelectableChannel中的register()方法进行注册的。
1
2
3
4
5
|
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。既然是关联对象,那肯定是可以得到连接的两个对象的:
1
2
|
public
abstract
SelectableChannel channel()
public
abstract
Selector selector()
|
还有支持的感兴趣的事件,以及已经准备好IO的事件,感兴趣的事件的方法是同名重载,一个为get另一个为set:
1
2
3
|
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是否已经关闭。
1
2
3
4
|
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,将调用如下方法进行处理。
1
|
public
abstract
Set selectedKeys( );
|
这个方法返回一个包含SelectionKey对象的集合,分别对应各个准备好的Channel。而对于注册在这个Selector的所有Key,还有一个方法可以获取到。
1
|
public
abstract
Set keys( );
|
Selector对象维护了3个key集合,一个注册过的,一个是选择过的,最后一个是cancel过但是未反注册的,这个我们没有方法直接获取到。
4. 常规使用示例
了解过了这3个重要角色,看一段常规使用的代码示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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( );
}
}
|
这是一个简单服务器接受请求,并做读取的代码逻辑。
1
|
readDataFromSocket (key);
|
这句是通过Channel和Buffer进行数据读取处理。
注意最后的:
1
|
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可以参看: