Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并判断是否为读写事件做好准备。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
在真正使用选择器时,涉及到的主要类有3个:
public abstract class SelectableChannel extends AbstractChannel implements Channel { public abstract SelectionKey register(Selector sel, int ops) throws ClosedChannelException; public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException; public abstract boolean isRegistered(); public abstract SelectionKey keyFor(Selector sel); public abstract int validOps(); public abstract void configureBlocking(boolean block) throws IOException; public abstract boolean isBlocking(); public abstract Object blockingLock(); }
调用register() 方法会将一个非阻塞通道注册到一个选择器上。如果试图注册一个处于阻塞状态的通道,register() 将抛出未检查的 IllegalBlockingModeException 异常。此外,通道一旦被注册,就不能回到阻塞状态。试图这么做的话,将在调用 configureBlocking() 方法时将抛出 IllegalBlockingModeException 异常。并且,理所当然地,试图注册一个已经关闭的 SelectableChannel 实例的话,也将抛出 ClosedChannelException 异常,就像方法原型指示的那样。通道在被注册到一个选择器上之前,必须先设置为非阻塞模式(通过调用configureBlocking(false))。如下:
channel.configureBlocking(false);// 设置为非阻塞模式 SelectionKey key = channel.register(selector,Selectionkey.OP_READ);// 调用方法进行
与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
register()方法的第二个参数可以指定监听四种不同类型的事件,这四种事件用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;具体的解释如下:
通道触发了一个事件意思是该事件已经就绪。如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
下面来看一下SelectionKey抽象类中定义的方法,如下:
public abstract class SelectionKey { public abstract SelectableChannel channel();// 获取Channel对象 public abstract Selector selector(); // 获取Selector对象 public abstract void cancel(); public abstract boolean isValid(); }
一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。前两个方法中反映了这种关系:channel() 方法返回与该键相关的 SelectableChannel 对象,而 selector() 则返回相关的 Selector 对象。
键对象表示了一种特定的注册关系。当应该终结这种关系的时候,可以调用 SelectionKey 对象的 cancel() 方法。可以通过调用 isValid() 方法来检查它是否仍然表示一种有效的关系。当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。
当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出 CancelledKeyException。
public abstract int interestOps(); public abstract void interestOps(int ops); public abstract int readyOps();
一个 SelectionKey 对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(instrest 集合),另一个表示通道准备好要执行的操作(ready 集合)。
当前的 interest 集合可以通过调用键对象的 interestOps() 方法来获取。最初,这应该是通道被注册时传进来的值。这个 interset 集合永远不会被选择器改变,但你可以通过调用 interestOps() 方法并传入一个新的比特掩码参数来改变它。
interest 集合也可以通过将通道注册到选择器上来改变(实际上使用一种迂回的方式调用 interestOps())。当相关的 Selector 上的 select() 操作正在进行时改变键的 interest 集合,不会影响那个正在进行的选择操作。所有更改将会在 select() 的下一个调用中体现出来。 interest集合是你所选择的感兴趣的事件集合。可以通过SelectionKey中的interestOps()方法读写interest集合,如下:
int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
可以看到,用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。
可以通过调用键的 readyOps() 方法来获取相关的通道的已经就绪的操作。ready 集合是 interest 集合的子集,并且表示了 interest 集合中从上次调用 select() 以来已经就绪的那些操作。 ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先调用readyOps()方法访问ready集合:
int readySet = selectionKey.readyOps();
就像之前提到过的那样,有四个通道操作可以被用于测试就绪状态。你可以像上面的代码那样,通过测试比特掩码来检查这些状态,但 SelectionKey 类定义了四个便于使用的布尔方法来为你测试这些比特值。每一个方法都与使用特定掩码来测试 readyOps( )方法的结果的效果相同:
public final boolean isReadable(); public final boolean isWritable(); public final boolean isConnectable(); public final boolean isAcceptable();
if (key.isWritable()) 等价于: if ((key.readyOps() & SelectionKey.OP_WRITE) != 0)这四个方法在任意一个 SelectionKey 对象上都能安全地调用。不能在一个通道上注册一个它不支持的操作,这种操作也永远不会出现在 ready 集合中。调用一个不支持的操作将总是返回 false,因为这种操作在该通道上永远不会准备好。
public final Object attach(Object ob); public final Object attachment();
这两 个方法允许你在键上放置一个“附件”,并在后面获取它。这是一种允许你将任意对象与键关联的便捷的方法。这个对象可以引用任何对你而言有意义的对象,例如业务对象、会话句柄、其他通道等等。这将允许你遍历与选择器相关的键,使用附加在上面的对象句柄作为引用来获取相关的上下文。
attach() 方法将在键对象中保存所提供的对象的引用。SelectionKey 类除了保存它之外,不会将它用于任何其他用途。任何一个之前保存在键中的附件引用都会被替换。可以使用 null 值来清除附件。可以通过调用 attachment() 方法来获取与键关联的附件句柄。
如果选择键的存续时间很长,但你附加的对象不应该存在那么长时间,请记得在完成后清理附件。否则,你附加的对象将不能被垃圾回收,你将会面临内存泄漏问题。
回忆下 SelectableChannel 的 register() 方法,有个接受一个 Object 参数的重载方法如下:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, myObject); 等价于: SelectionKey key = channel.register(selector, SelectionKey.OP_READ); key.attach (myObject);
选择器维护着注册过的通道的集合,并且这些注册关系中的任意一个都是封装在 SelectionKey 对象中的。每一个 Selector 对象维护三个键的集合:
public abstract class Selectory { // This is a partial API listing 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(); }
public abstract void wakeup();有三种方式可以唤醒在 select() 方法中睡眠的线程: