目录
Selector的重要属性
- keys:该集合中保存了所有注册到当前选择器上的通道的SelectionKey对象;
- selectedKeys:该集合中保存了上一次Selector选择期间,发生了就绪事件的通道的SelectionKey对象集合,它始终是keys的子集。
- cancelledKeys:该集合保存了已经被取消但其关联的通道还未被注销的SelectionKey对象集合,它始终是keys的子集。
初始化Selector对象时,这三个集合都为空。当我们调用Channel的register方法将通道注册到选择器时,一个SelectionKey对象会被加入到keys集合;
调用通道的Close方法或直接调用选择器的cancel方法则会将一个SelectionKey对象添加到cancelledKeys集合,选择器下一次做选择操作时,将会清空cancelledKeys中保存的选择键,并从keys集合中删除;
选择器做选择操作时,具有就绪事件的SelectionKey对象会被加入到selectedKeys集合中。
每个Selector中还维护了publicKeys和publicSelectedKeys两个视图,供客户端使用。publicKeys是keys的视图,调用Selector的keys()方法返回的就是publicKeys,publicKeys不支持添加和删除操作;publicSelectedKeys是selectedKeys的视图,它是是个不可增长的集合,即不支持add操作,但支持remove操作,调用publicSelectedKeys集合的remove操作实际是从selectedKeys中删除一个SelectionKey对象。我们可以调用Selector的selectedKeys()方法访问publicSelectedKeys。
Selector的创建
Selector selector = Selector.open();
open 方法:open方法负责向SPI发出请求,获取一个SelectorProvider实例。然后对应的provider调用openSelector,获取具体实现的selector
public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); }
provider()方法:SelectorProvider的静态工厂方法 provider()决定由哪个SelectorProvider对象来创建给定的Selector实例,通常是一个DefaultSelectorProvider实例。不同操作系统对应着不同的sun.nio.ch.DefaultSelectorProvider,Linux下DefaultSelectorProvider.create()会生成一个sun.nio.ch.EPollSelectorProvider类型的SelectorProvider,Windows环境下则生成sun.nio.ch.WindowsSelectorProvider类型的SelectorProvider。
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
Selector的注册
根据channel与selector生成相应的SelectionKeyImpl,其构造方法就是简单的保存了传递进来的参数,并且将所需要attach的object进行保存。之后,调用子类的implRegister()方法。然后修改它的的interestOpts
protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
if (!(var1 instanceof SelChImpl)) {
throw new IllegalSelectorException();
} else {
SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
var4.attach(var3);
Set var5 = this.publicKeys;
synchronized(this.publicKeys) {
this.implRegister(var4);
}
var4.interestOps(var2);
return var4;
}
}
SelectionKey对象代表着一个Channel和它注册的Selector间的关系。其channel( )方法可返回与该键相关的SelectableChannel对象,而selector( )则返回相关的Selector对象。SelectionKey中包含两个重要属性,两个以整数形式进行编码的比特掩码:
- interestOps:代表对注册Channel所感兴趣的事件集合。interest集合是使用注册通道时给定的值初始化的,可以通过调用键对象的interestOps( int ops)方法修改。同时,可以调用键对象的interestOps()方法获取当前interest集合。当相关的Selector上的select( )操作正在进行时改变键的interest集合,不会影响那个正在进行的选择操作。所有更改将会在select( )的下一个调用中体现出来;
- readyOps:代表interest集合中从上次调用select( )以来已经就绪的事件集合,它是interestOps的子集。注册通道时,初始化为0,只有在选择器选择操作期间可能被更新。可以调用键对象的readyOps()方法获取当前ready集合。需注意的是ready集合返回的就绪状态只是一个提示
Selector的select的操作
- select():该方法会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非当前线程发生中断或selector的wakeup方法被调用;
- select(long timeout):该方法与select()类似,会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非下面3种情况任意一种发生:1 设置的超时时间到达;2 当前线程发生中断;3 selector的wakeup方法被调用;
- selectNow():该方法不会发生阻塞,无论是否有channel发生就绪事件,都会立即返回。
select()大致的过程
- 检查cancelledKeys集合,如果它非空,从keys集合中移除所有存在于cancelledKeys集合中的SelectionKey对象,并将注销其通道,同时清空cancelledKeys;
- 向内核发起一个系统调用进行查询,以确定选择器上注册的每个通道所关心的事件是否就绪。如果没有通道已经准备好,线程可能会一直阻塞、阻塞指定时间,或立即返回,这主要依赖于特定select方法的调用;
- 系统调用返回,再次检查cancelledKeys集合;
- 系统调用返回后,对于那些没有就绪事件的通道将不会有任何的操作,对于那些已经有就绪事件的通道:1 如果通道的SelectionKey还未加入selectedKeys集合,将其添加到selectedKeys集合中,并修改ready集合,以便准确地标识该通道当前有哪些准备好的操作。先前记录在ready集合中的任何就绪信息都会被抛弃;2 通道的SelectionKey已经存在于selectedKeys集合,修改ready集合,以便准确地标识该通道当前有哪些准备好的操作。所有之前记录在ready集合中已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的ready集合是与之前的ready集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的ready集合将是累积的。比特位只会被设置,不会被清理。
select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是selectedKeys集合中的通道总数。是这一次就绪的数量
Selector的唤醒wakeup()
- selector.select()是阻塞的,通常情况下,只有注册在selector上的channel有事件就绪时,select()才会从阻塞中被唤醒,处理就绪事件。如果想在没有事件就绪时唤醒select线程,需要调用wakeup()方法。
wakeup()实现的功能:
- 如果一个线程在调用select()或select(long)方法时被阻塞,调用wakeup()会使线程立即从阻塞中唤醒;
- 如果调用wakeup()期间没有select操作,下次调用select相关操作会立即返回,不会执行poll(),包括调用selectNow()。
- 在select期间,多次调用wakeup()与调用一次效果是一样的。
- 如果调用wakeup()期间没有select操作,后续若先调用一次selectNow(),再次调用select()则会导致阻塞。
wakeup()实现:
- 实例化SelectorImpl时,会调用Pipe.open()创建一个管道实例wakeupPipe,并从wakeupPipe中获取wakeupSourceFd和wakeupSinkFd两个文件描述符,wakeupSourceFd为read端FD,wakeupSinkFd为write端FD,然后将wakeupSourceFd加入pollWrapper中。
- pollWrapper的作用是保存当前selector对象上注册的FD,当调用Selector的select()方法时,会将pollWrapper的内存地址传递给内核,由内核负责轮训pollWrapper中的FD,一旦有事件就绪,将事件就绪的FD传递回用户空间,阻塞在select()的线程就会被唤醒。
- wakeup通过wakeupSinkFd发送信号到wakeupSourceFd,让Selector的select()方法检测到wakeupSourceFd已经就绪,从而打破了阻塞,做到唤醒。
windows环境下wakeup()的实现原理,它通过一个可写的SinkChannel和一个可读的SourceChannel组成的pipe来实现唤醒的功能
Linux环境则使用其本身的Pipe来实现唤醒功能。
无论windows还是linux,wakeup的思想是完全一致的,只不过windows没有Pipe这种信号通知的机制,所以通过TCP来实现了Pipe。
每创建一个Selector对象,都会创建一个Pipe实例,这会导致消耗两个文件描述符FD和两个端口号
WindowsSelectorImpl 源码解析
初始化的时候创建了pipe
对象成员,一个通道,用作唤醒
private final Pipe wakeupPipe = Pipe.open();
初始化代码 将通道的读端加入了pollWrapper
WindowsSelectorImpl(SelectorProvider var1) throws IOException {
// 初始化Selector
super(var1);
this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
// 不适用延迟,有任何字节立马发送
var2.sc.socket().setTcpNoDelay(true);
this.wakeupSinkFd = var2.getFDVal();
// 将wakeupSourceFd加入pollWrapper,关注通道是否有数据
this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
}
wakeup代码
public Selector wakeup() {
Object var1 = this.interruptLock;
synchronized(this.interruptLock) {
// 收到中断。多次收到中断并不会多次发送
if (!this.interruptTriggered) {
// 向wakeupSinkFd 发送一个字节,让wakeupSourceFd就绪
this.setWakeupSocket();
this.interruptTriggered = true;
}
return this;
}
}
在select过程中 会首先判断是否收到了中断,如果收到中断就不会poll了,会直接重置中断,然后返回0。
参考链接