引言
上篇文章我们简单的使用了NIO的Channel通道,本期我们主要来介绍一下选择器(Selector
)的使用,Selector是Java NIO核心组件中的一个。在之前介绍5种I/O模型的时候,有介绍过多路复用模型。多路复用模型使得我们可以使用一个线程来管理成千上万的连接,避免了线程上下文切换带来的开销,使得性能得到极大的提升
基本模型
选择器Selector使用的基本模式,跟传统BIO处理模型不一样。传统BIO往往我们会使用多线程来提升处理性能,也就是说每接入一个Client,Server端就会为其新开一个线程,以此来提升并发吞吐量,使用这种模式的弊端很明显,因为线程是系统非常重要的资源,当并发量少的时候,感觉不到,一旦并发量上来,就会出现瓶颈。
再来看Selector是如何处理的,首先每接入一个Client,我们可以通过Selector选择器注册感兴趣的事件,然后通过一个线程去不停的轮询检测各个Client是否有感兴趣的事件发生,有则顺序处理该Client就绪的各个事件。
image.png
Selector 使用实例
我们先来看一个Selector非常常见的使用例子
try {
// 打开一个通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8890));
// 打开 选择器 selector
Selector selector = Selector.open();
// 设置非阻塞
serverSocketChannel.configureBlocking(false);
// 为ServerSocketChannel注册 OP_ACCEPT 事件,返回一个SelectionKey 对象
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 返回至少有一个事件就绪的通道数,该方法是阻塞的
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}
// 返回就绪的 SelectionKey集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iteratorKeys = selectionKeys.iterator();
// 遍历所有就绪的SelectionKey集合
while (iteratorKeys.hasNext()) {
SelectionKey key = iteratorKeys.next();
iteratorKeys.remove();
// 判断就绪的具体事件
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受一个新连接
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
// 为该Channel注册可读事件
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
...
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
通过查看上面的代码,可知我们通过Selector.open()创建了一个选择器,并且通过SocketChannel的register方法注册了选择器和事件,其中register方法会返回一个SelectionKey对象,该对象其实就是维护了Channel和Selector的对应关系
SelectionKey
上文我们提到SelectionKey对象维护了Channel和Selector的对应关系,现在我们来看下
SelectionKey对象内部几个非常重要的属性和方法
-
属性
- OP_READ OP_WRITE OP_CONNECT OP_ACCEPT 可注册的四个感兴趣的项目
- interestOps 所有感兴趣的集合
- readyOps 所有就绪的兴趣集合
-
方法
- boolean isValid() 判断该
SelectionKey是否有效 - void cancel() 取消该
SelectionKey中 通道与其选择器 的注册 - int interestOps() 返回该
SelectionKey所包含的所有兴趣(可读可写)集合 - SelectionKey interestOps(int ops) 设置一个感兴趣的项目
- int readyOps() 返回已经就绪的兴趣集合
- boolean isWritable() 判断是否可写
- boolean isReadable() 判断是否可读
- boolean isConnectable() 判断是否可连接
- boolean isAcceptable() 判断是否可接受
- Object attach(Object ob) 在该
SelectionKey中附加一个对象信息 - Object attachment() 获取附加对象信息
- boolean isValid() 判断该
Selector
讲完SelectionKey和Selector的关系之后,我们再次回到Selector类,我们首先需要知道Selector中3个重要的SelectionKey集合
-
keys:所有注册到Selector的Channel所表示的SelectionKey都会存在于该集合中。keys元素的添加会在Channel注册到Selector时发生。 -
selectedKeys:该集合中的每个SelectionKey都是其对应的Channel在上一次操作selection期间被检查到至少有一种SelectionKey中所感兴趣的操作已经准备好被处理。该集合是keys的一个子集。 -
cancelledKeys:执行了取消操作的SelectionKey会被放入到该集合中。该集合是keys的一个子集。
接下来将会介绍上文例子中所用到的几个方法
Selector.open()
这个静态方法可以打开一个Selector选择器
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
通过查看源码可知在Linux系统下默认会使用EpollSelectorImpl来作为Selector实现类,注意各个系统下默认的实现类是不同的。
selector.select()
Selector的select()方法会返回事件就绪的SelectionKey数目(也就是就绪的Channel数)
并且该方法会一直阻塞直到至少一个channel被选择(即,该channel注册的事件发生了)为止,除非当前线程发生中断或者selector的wakeup方法被调用
该方法还有一个重载select(long timeout),可以自定义超时时间
selector.selectNow()
该方法与上面方法类似,但该方法不会发生阻塞,即使没有一个channel被选择也会立即返回
selector.selectedKeys()
返回已就绪的SelectionKey集合,该方法在执行了selector.select()后调用,因为在执行selector.select()后就表示至少有一个SelectionKey已经就绪
尾言
好了,本篇文章就介绍到这里了,篇幅有限加上本人对NIO的理解也有待加深,希望可以在之后更深入的对Java NIO的实现进行分析。












网友评论