目录:
Java NIO 学习笔记(一)----概述,Channel/Buffer
Java NIO 学习笔记(二)----聚集和分散,通道到通道
Java NIO 学习笔记(三)----Selector
Java NIO 学习笔记(四)----文件通道和网络通道
Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe
Java NIO 学习笔记(六)----异步文件通道 AsynchronousFileChannel
Java NIO 学习笔记(七)----NIO/IO 的对比和总结
选择器是一个 NIO 组件,它可以检测一个或多个 NIO 通道,并确定哪些通道可以用于读或写了。 这样,单个线程可以管理多个通道,从而管理多个网络连接。
摘要:一个选择器可对应多个通道,选择器是通过 SelectionKey 这个关键对象完成对多个通道的选择的。注册选择器的时候会返回此对象,调用选择器的 selectedKeys() 方法也会返回此对象。每一个 SelectionKey 都包含了一些必要信息,比如关联的通道和选择器,获取到 SelectionKey 后就可以从中取出对应通道进行操作。
为什么使用选择器?
仅使用单个线程来处理多个通道的优点是,只需要更少的线程来处理通道。 实际上只需使用一个线程来处理所有通道。 对于操作系统而言,在线程之间切换是昂贵的,并且每个线程也占用操作系统中的一些资源(存储器)。 因此,使用的线程越少越好。
但请记住,现代操作系统和 CPU 在多任务处理中变得越来越好,因此随着时间的推移,多线程的开销会变得越来越小。 事实上,如果一个 CPU 有多个内核,你可能会因多任务而浪费 CPU 能力。 无论如何,这里知道可以使用选择器使用单个线程处理多个通道就可以。
以下是使用 1 个 Selector 处理 3 个 Channel 的线程图示:
使用选择器注册通道
首先创建一个选择器,它是通过这种方式创建的:
Selector selector = Selector.open();
要使用带选择器的通道,必须使用选择器来注册通道。 这是使用关联 Channel 对象的 register() 方法完成的,如下所示:
channel.configureBlocking(false); //不阻塞
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 使用通道注册一个选择器
通道必须处于非阻塞模式才能与选择器一起使用。 这意味着无法将 FileChannel 与 Selector一 起使用,因为 FileChannel 无法切换到非阻塞模式。 套接字通道则支持。
注意 register() 方法的第二个参数。 这是一个“ interest 集合”,意味着通过 Selector 在 Channel 中监听哪些事件。可以收听四种不同的事件:
- Connect 连接
- Accept 接收
- Read 读
- Write 写
一个“发起事件”的通道也被称为“已就绪”事件。 因此,已成功连接到另一台服务器的通道是“连接就绪”。 接受传入连接的服务器套接字通道是“接收就绪”。 准备好要读取的数据的通道“读就绪”。 准备好写入数据的通道称为“写就绪”。
这四个事件由四个 SelectionKey 常量表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果要监听多个事件,那么可以用“|”位或操作符将常量连接起来,如下所示:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
本文后面再进一步回顾 interest 集合。
register() 方法返回的 SelectionKey 对象
正如在上一节中看到的,当使用 Selector 注册 Channel 时,register() 方法返回一个 SelectionKey 对象。 这个 SelectionKey 对象包含一些有趣的属性:
- interest 集合
- ready 集合
- 对应 Channel
- 对应 Selector
- 附加对象(可选)
interest 集合
interest 集合是所选择的感兴趣的事件集合,可以通过 SelectionKey 读取和写入 Interest 集合,如下所示:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
可以使用给定的 SelectionKey 常量和 interest 集合进行“&”位与操作,以查明某个事件是否在 interest 集合中。
ready 集合
就绪集是通道准备好的一组操作。 将在 Selector 后访问就绪集,可以像这样访问 ready set:
int readySet = selectionKey.readOps();
可以使用与上面 interest 集合相同的方式,使用位与操作进行检测频道已准备好的事件/操作。 但是,也可以使用下面这四种方法,它们都会返回一个布尔值:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
对应 Channel + Selector
从 SelectionKey 访问通道和选择器非常简单:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
附加对象(可选)
可以将对象或者更多信息附加到 SelectionKey ,这是识别某个通道的便捷方式。 例如,可以将正在使用的缓冲区与通道或其他对象相关联。 以下是使用方法:
// 将 theObject 对象附加到 SelectionKey
selectionKey.attach(theObject);
// 从 SelectionKey 中取出附加的对象
Object attachedObj = selectionKey.attachment();
还可以在 register() 方法中添加参数,在使用 Selector 注册 Channel 时就附加对象。如下:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通过选择器选择通道
使用 Selector 注册一个或多个通道后,可以调用其中一个 select() 方法。 这些方法返回我们感兴趣的,已就绪的事件(连接,接受,读写)的通道。 换句话说,如果对读就绪通道感兴趣,select() 方法会返回读事件已经就绪的那些通道
以下是 select() 方法:
- int select() : 将一直阻塞,直到至少有一个频道为注册的事件做好准备。
- int select(long timeout) :与 select() 相同,但它会最长阻塞 timeout 毫秒。
- int selectNow() :完全没有阻塞。 它会立即返回任何已准备好的通道。
select() 方法返回的 int 表示有多少通道准备好了。也就是说,自从你上次调用 select() 以来,有多少频道已经准备好了。
如果调用 select() ,因为一个频道已准备就绪,它会返回 1 ,再次调用 select() ,因为另外一个通道已准备就绪,它会再次返回 1 。如果没有对第一个已准备就绪的通道做任何事情,那么现在就有 2 个准备就绪的频道,但是在每次 select() 调用之间,只有一个通道是准备就绪的。
选择器的 selectedKeys() 方法返回的 SelectionKey 集合
一旦调用了其中一个 select() 方法并且其返回值表示有通道已准备就绪,就可以通过调用选择器的 selectedKeys() 方法,因为一个选择器可以注册多个通道,所以这里返回集合。通过“已选择键集(selected key set)”访问就绪通道。 如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
使用 Selector 注册通道时,Channel 对象的 register() 方法返回 SelectionKey 对象。此对象代表了该选择器注册的通道。
可以迭代 selectedKeys() 方法返回的 Set 集合来访问就绪通道。如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// ServerSocketChannel接受了一个连接。
} else if (key.isConnectable()) {
// 与远程服务器建立连接。
} else if (key.isReadable()) {
// 一个通道已读就绪
} else if (key.isWritable()) {
// 一个通道已写就绪
}
keyIterator.remove();
}
此循环迭代 Set,对于每个 key ,它测试 key 以确定 key 引用的通道已准备就绪的事件。
注意选择器不会从 Set 本身中删除 SelectionKey 对象。 完成通道处理后,必须在每次迭代结束时的调用 keyIterator.remove()
来删除集合中已处理过的 SelectionKey 。 下一次通道变为“就绪”时,选择器会再次将其添加到选择键集中。
这里 Set 中的 SelectionKey 和当时使用 Selector 注册 Channel 返回的 SelectionKey 是一样的,请参考上述。
调用其对象方法 selectionKey.channel();
就会返回 Channel 对象,这时候我们应该将其转换为具体需要使用的通道,例如 ServerSocketChannel 或 SocketChannel 等。
wakeUp() 唤醒被阻塞的线程
已调用 select() 方法的线程可能会被阻塞,这是可以通过调用 wakeUp() 方法离开 select() 方法,即使尚未准备好任何通道。其它线程来调用阻塞线程 Selector 对象的 select() 即可让阻塞在 select() 方法上的线程立马返回。
如果另一个线程调用 wakeup() 并且当前在 select() 中没有阻塞线程,则调用 select() 的下一个线程将立即被“唤醒”。
close() 关闭选择器
调用选择器的 close() 方法将关闭 Selector 并使使用此 Selector 注册的所有 SelectionKey 实例失效。 但通道本身并不会被关闭。
Selector 选择器总结
下面是一个完整的例子,它打开一个 Selector ,用它注册一个通道(因为通道相关在后面,还未学习,这里通道实例化被省略),并继续监视 Selector 以获得四个事件的“准备就绪”(接受,连接,读取,写入)。
Selector selector = Selector.open(); // 打开选择器
channel.configureBlocking(false); // 设置不阻塞,因为通道必须处于非阻塞模式才能与选择器一起使用
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 使用通道注册一个选择器
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
// 这里的 SelectionKey 就和注册时候返回的 key 一样,
// 因为一个选择器可以注册多个通道,所以这里返回集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// ServerSocketChannel接受了一个连接。
} else if (key.isConnectable()) {
// 与远程服务器建立连接。
} else if (key.isReadable()) {
// 一个通道已读就绪
} else if (key.isWritable()) {
// 一个通道已写就绪
}
keyIterator.remove();
}
}
再回顾一下:
- Selector.open() 打开选择器,设置通道不阻塞,调用通道的 register() 方法注册选择器,此方法的第二个参数是一个“ interest 集合”(Connect 、Accept 、Read 、Write )
- register() 方法返回一个 SelectionKey 对象,此对象包含了一些注册信息(interest 集合,ready 集合,对应 Channel,对应 Selector,附加对象(可选)),可以调用此对象的一些方法返回一些很有用的信息,例如
Channel channel = selectionKey.channel();
返回关联的通道。 - 使用 Selector 注册一个或多个通道后,可以调用其中一个 select() 方法来选择通道,选择什么通道呢?选择我们注册时候, interest 集合里面所关注的所有通道,然后返回被选择的已准备就绪的通道数量,如果此方法返回值不为 0 ,代表 selector 对象里面有包含我们需要的通道了。
- 知道有就绪通道后,可以使用
selector.selectedKeys()
方法获取 SelectionKey 集合,对于集合中每一个 SelectionKey 都包含了一些必要信息,比如关联的通道和选择器,注意一个选择器可对应多个通道。获取到 SelectionKey 后就可以从中取出对应通道进行操作,这也是选择器的作用所在,一个选择器,操作多个通道。