Java NIO6:选择器1——理论篇
一、选择器
选择器提供选择执行已经就绪的任务的能力,这使得多元I/O成为了可能,就绪执行和多元选择使得单线程能够有效地同时管理多个I/O通道。
某种程度上来说,理解选择器比理解缓冲区和通道类更困难一些和复杂一些,因为涉及了三个主要的类,它们都会同时参与到这整个过程中,这里先将选择器的执行分解为几条细节:
1、创建一个或者多个可选择的通道(SelectableChannel)
2、将这些创建的通道注册到选择器对象中
3、选择键会记住开发者关心的通道,它们也会追踪对应的通道是否已经就绪
4、开发者调用一个选择器对象的select()方法,当方法从阻塞状态返回时,选择键会被更新
5、获取选择键的集合,找到当时已经就绪的通道,通过遍历这些键,开发者可以选择对已就绪的通道要做的操作
对于选择器的操作,大致就是这么几步,OK,接下去再进一步,看一下和选择器相关的三个类。
二、选择器、可选择通道和选择键类
1、选择器(Selector)
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。
2、可选择通道(SelectableChannel)
这个抽象类提供了实现通道的可选择性所需要的公共方法,它是所有支持就绪检查的通道类的父类,FileChannel对象不是可选择的,因为它们没有继承SelectableChannel,所有Socket通道都是可选择的,包括从管道(Pipe)对象中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以设定对哪个选择器而言哪种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
3、选择键(SelectionKey)
选择键封装了特定的通道与特定的选择器的注册关系。调用SelectableChannel.register()方法会返回选择键并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
用一张UML图来描述一下选择器、可选择通道和选择键:
三、建立选择器
前面讲了,选择器的作用是管理了被注册的通道集合和它们的就绪状态,假设我们有三个Socket通道的选择器,可能会有类似的代码:
... Selector selector = Selector.open(); channel1.register(selector, SelectionKey.OP_READ); channel2.register(selector, SelectionKey.OP_WRITE); channel3.register(selector, SelectionKey.OP_READ | OP_WRITE); channel4.register(selector, SelectionKey.OP_READ | OP_ACCEPT); ready = selector.select(10000); ...
这种操作用图表示就是:
代码创建了一个新的选择器,然后将这四个(已经存在的)Socket通道注册到选择器上,而且感兴趣的操作各不相同。select()方法在将线程置于睡眠状态直到这些感兴趣的事件中的一个发生或者10秒钟过去,这就是所谓的事件驱动。
再稍微看一下Selector的API细节:
public abstract class Selector { ... public static Selector open() throws IOException; public abstract boolean isOpen(); public abstract void close() throws IOException; public abstract SelectionProvider provider(); ... }
Selector是通过调用静态工厂方法open()来实例化的,这个从前面的代码里面也看到了,选择器不是像通道或流那样的基本I/O对象----数据从来没有通过他们进行传递。
通道是调用register方法注册到选择器上的,从代码里面可以看到register()方法接受一个Selector对象作为参数,以及一个名为ops的整数型参数,第二个参数表示关心的通道操作。在JDK1.4中,有四种被定义的可选择操作:读(read)、写(write)、连接(connect)和接受(accept)。
注意并非所有的操作都在所有的可选择通道上被支持,例如SocketChannel就不支持accept。
四、使用选择键
接下来看看选择键,选择键的API大致如下:
public abstract class SelectionKey { public static final int OP_READ; public static final int OP_WRITE; public static final int OP_CONNECT; public static final int OP_ACCEPT; public abstract SelectableChannel channel(); public abstract Selector selector(); public abstract void cancel(); public abstract boolean isValid(); public abstract int interestOps(); public abstract void iterestOps(int ops); public abstract int readyOps(); public final boolean isReadable(); public final boolean isWritable(); public final boolean isConnectable(); public final boolean isAcceptable(); public final Object attach(Object ob); public final Object attachment(); }
关于这些API,总结几点:
1、就像前面提到的,一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系,channel()方法和selector()方法反映了这种关系
2、开发者可以使用cancel()方法终结这种关系,可以使用isValid()方法来检查这种有效的关系是否仍然存在,可以使用readyOps()方法来获取相关的通道已经就绪的操作
3、第2点有提到readyOps()方法,不过我们往往不需要使用这个方法,SelectionKey类定义了四个便于使用的布尔方法来为开发者测试通道的就绪状态,例如:
if (key.isWritable()){...}
这种写法就等价于:
if ((key.readyOps() & SelectionKeys.OPWRITE) != 0){...}
isWritable()、isReadable()、isConnectable()、isAcceptable()四个方法在任意一个SelectionKey对象上都能安全地调用。
4、当通道关闭时,所有相关的键会自动取消(一个通道可以被注册到多个选择器上);当选择器关闭时,所有被注册到该选择器的通道都会被注销并且相关的键立即被取消。
五、Selector维护的三种键
选择器维护者注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey对象中的。每一个Selector对象维护三种键的集合:
public abstract class Selector { ... public abstract Set keys(); public abstract Set selectedKeys(); public abstract int select() throws IOException; public abstract int select(long timeout) throws IOException; public abstract int selectNow() throws IOException; public abstract void wakeup(); ... }
由这个API看下去,这三种键是:
已注册的键的集合(Registered key set)
与选择器关联的已经注册的键的集合,并不是所有注册过的键都有效,这个集合通过keys()方法返回,并且可能是空的。这些键的集合是不可以直接修改的,试图这么做将引发java.lang.UnsupportedOperationException。
已选择的键的集合(Selected key set)
已注册的键的集合的子集,这个集合的每个成员都是相关的通道被选择器判断为已经准备好的并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(有可能是空的)。
键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。
已取消的键的集合(Cancelled key set)
已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。
六、选择过程
接着就是Selector的核心选择过程了。基本上来说,选择器是对select()、poll()、epoll()等本地调用或者类似的操作系统特定的系统调用的一个包装。但是Selector所做的不仅仅是简单地向本地代码传送参数,每个操作都有特定的过程,对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。
选择操作是当三种形式的select()中的任意一种被调用时,由选择器执行的。不管是哪一种形式的调用,下面步骤将被执行:
1、已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。此步骤结束,已取消的键的集合将是空的。
2、已注册的键的集合中的键的interest集合将被检查,此步骤结束,对interest集合的改动不会影响剩余的检查过程。一旦就绪条件被定下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态,依赖于特定的select()方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。
3、步骤2可能会花费很长时间,特别是线程处于阻塞状态时。与该选择器相关的键可能会同时被取消,当步骤2结束时,步骤1将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注册。
4、select操作的返回值不是已准备好的通道的总数,而是从上一个select()调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。
最后,上面的Selector中还有两个方法没有提到,这里说明一下它们的意思:
1、selectNow()
调用selectNow()方法执行就绪检查过程,但不阻塞,如果当前没有通道就绪,立刻返回0.
2、wakeup()
调用wakeup()方法将使得选择器上的第一个还没有返回的选择操作立即返回,如果当前没有正在进行中的选择,那么下一次对select()方法的一种形式的调用将立即返回,后续的选择操作将正常进行。