前言: 通过register 方法将通道注册到selector 之后,接下来就是要去监听对应的通道数据是否准备就绪;
selector.select() 调用SelectorImpl 中的select():
public int select() throws IOException {return this.select(0L);
}
public int select(long var1) throws IOException {if (var1 < 0L) {throw new IllegalArgumentException("Negative timeout");} else {return this.lockAndDoSelect(var1 == 0L ? -1L : var1);}
}
private int lockAndDoSelect(long var1) throws IOException {synchronized(this) {if (!this.isOpen()) {// selector 是否打开throw new ClosedSelectorException();} else {int var10000;synchronized(this.publicKeys) {synchronized(this.publicSelectedKeys) {// 获取当前selector 的记录注册通的key 和 记录有数据准备好的通道keyvar10000 = this.doSelect(var1);}}return var10000;}}
}
3种类型的实现默认都是调用lockAndDoSelect(long timeout),只是各自设置的超时时间不同。
- select():超时时间为-1L,即永不会超时。选择器会一直阻塞到至少一个channel中有感兴趣的事件发生,除非当前线程发生中断或 selector 的 wakeup 方法被调用;
- selectNow():超时时间为0L,调用该方法,选择器不会被阻塞,无论是否有channel发生就绪事件,都会立即返回;
- select(long timeout):超时时间由用户自定义,但不能小于0L;会一直阻塞直到至少一个 channel 中有感兴趣的事件发生,除非下面 3 种情况任意一种发生:1 设置的超时时间到达;2 当前线程发生中断;3 selector 的 wakeup 方法被调用当选择器上没有channel发生就绪事件。当然也可设置为0L降级为select()调用。
WindowsSelectorImpl 类中的doSelect 方法:
protected int doSelect(long var1) throws IOException {if (this.channelArray == null) {throw new ClosedSelectorException();} else {// 超时参数this.timeout = var1;// 移除已经关闭的通道this.processDeregisterQueue();if (this.interruptTriggered) {this.resetWakeupSocket();return 0;} else {// 赋值需要辅助线程的数量this.adjustThreadsCount();// 赋值需要等待完成的现车数量this.finishLock.reset();// 唤醒所有辅助线程,一起向内核索要管道数据this.startLock.startThreads();try {this.begin();try {// 主线程先内核索要socket 管道数据this.subSelector.poll();} catch (IOException var7) {this.finishLock.setException(var7);}if (this.threads.size() > 0) {// 如果有辅助线程赋值获取管道数据,则主线程等待辅助线程完成数据获取this.finishLock.waitForHelperThreads();}} finally {this.end();}// 异常检查this.finishLock.checkForException();// 清除取消 的通道this.processDeregisterQueue();// 处理已经准备好的通道int var3 = this.updateSelectedKeys();// 标记唤醒标记this.resetWakeupSocket();return var3;}}
}
doSelect 的核心就是使用poll 函数向内核发起数据是否准备好的请求,如果 Selector 管理大量 Channel 时,Selector 使用了多线程 高效完成所有 Channel 上就绪事件的检查;
先看adjustThreadsCount 辅助线程数判断:
private int totalChannels = 1;private int threadsCount =0;private final List<WindowsSelectorImpl.SelectThread> threads = new ArrayList();private void adjustThreadsCount() {int var1;if (this.threadsCount > this.threads.size()) {// 所需要的线程数量小于当前线程数量--增加线程for(var1 = this.threads.size(); var1 < this.threadsCount; ++var1) {WindowsSelectorImpl.SelectThread var2 = new WindowsSelectorImpl.SelectThread(var1);this.threads.add(var2);// 守护线程设置var2.setDaemon(true);var2.start();}} else if (this.threadsCount < this.threads.size()) {// 所需要的线程数量大于当前线程数量--减少线程for(var1 = this.threads.size() - 1; var1 >= this.threadsCount; --var1) {((WindowsSelectorImpl.SelectThread)this.threads.remove(var1)).makeZombie();}}}
threadsCount 代表当前需要的辅助线程数量,this.threads 中则保存了上一次 select 操作需要的辅助线程。this.threadsCount > this.threads.size (),说明自上一次调用 select 以来,选择器上又新注册了通道。那么需要增加辅助线程,将新增的线程加入 threads 数组,然后设置线程为守护线程并立即启动。
this.threadsCount <this.threads.size (),说明自上一次调用 select 以来,有通道已经从选择器上注销。这时候需要从 threads 数组中移除多余的辅助线程。
在看WindowsSelectorImpl.SelectThread(var1):
// 继承线程
private final class SelectThread extends Thread {private final int index;final WindowsSelectorImpl.SubSelector subSelector;private long lastRun;private volatile boolean zombie;private SelectThread(int var2) {this.lastRun = 0L;// 初始值this.index = var2;// 下标,用于计算本线程需要监听的socketthis.subSelector = WindowsSelectorImpl.this.new SubSelector(var2);// 赋值lastRun为当前selector.select() 时startLock.runsCounter 的数字// 方便本批次线程在start 后统一进行wait 使用this.lastRun = WindowsSelectorImpl.this.startLock.runsCounter;}void makeZombie() {this.zombie = true;}boolean isZombie() {return this.zombie;}// 辅助线程先内核获取响应socket 的数据public void run() {for(; !WindowsSelectorImpl.this.startLock.waitForStart(this); WindowsSelectorImpl.this.finishLock.threadFinished()) {try {// 可以看到使用poll 获取内核数据this.subSelector.poll(this.index);} catch (IOException var2) {WindowsSelectorImpl.this.finishLock.setException(var2);}}}
}
当启动新的辅助线程时,实际该线程并不会立即向内核发起系统调用,WindowsSelectorImpl.this.startLock.waitForStart(this) 判断如果当前线程多余则跳出for 循环;
在看 WindowsSelectorImpl.this.startLock.waitForStart(this) :
private final class StartLock {private long runsCounter;private StartLock() {}private synchronized void startThreads() {++this.runsCounter;// 将 runsCounter +1this.notifyAll();}private synchronized boolean waitForStart(WindowsSelectorImpl.SelectThread var1) {while(this.runsCounter == var1.lastRun) {try {// 当前线程等待WindowsSelectorImpl.this.startLock.wait();} catch (InterruptedException var3) {Thread.currentThread().interrupt();}}if (var1.isZombie()) {// 线程多余返回true 在上一步for 循环中 判断条件不成立跳出for 循环return true;} else {var1.lastRun = this.runsCounter;return false;}}
}
可以看到 当 this.runsCounter == var1.lastRun 想等是start 的线程都会进入wait 等待,这里每一次selector.select() 时this.runsCounter都是相同的;当主线程调用:this.startLock.startThreads();
后唤醒所有的子线程进行poll 函数的调用:
private synchronized void startThreads() {++this.runsCounter;// 将 runsCounter +1this.notifyAll();
}
startThreads () 会将 runsCounter 加 1,而 waitForStart 则会将辅助线程的 lastRun 更新为 runsCounter。也就是说,每一次 doSelect 完成之后,辅助线程调用 startLock 的 waitForStart () 方法条件 this.runsCounter == thread.lastRun 总是成立,所以辅助线程在完成一次 doSelect 之后,就会进入等待状态。
只有在下一次 doSelect 调用时,主线程调用 startThreads (),将 runsCounter 加 1,同时调用 notifyAll () 唤醒所有处于等待状态的辅助线程,此时等待条件将不成立,所有的辅助线程都会参与到 CPU 调度中,准备向内核发起 poll 调用。由于 waitForStart 和 startThreads () 都是同步方法,保证了更新 runsCounter 的原子性和可见性。所以一旦调用了 startThreads (),则会更新 runsCounter 和唤醒等待在 waitForStart 的线程。
WindowsSelectorImpl 通过使用 startLock 来实现了协调所有线程同时向内核发起一个系统调用,高效完成所有 Channel 上就绪事件的检查。
此时主线程和子线程都调用了 this.subSelector.poll(); 向内核发起请求,阻塞等待事件返回;这里有两种情况,第一种辅助线程监听的socket 有事件返回,第二种主线程中监听的socket 有事件返回,不管哪种情况,都需要唤醒处理处于poll()函数阻塞的线程:
先看第一种 辅助线程返回:辅助线程返回执行 WindowsSelectorImpl.this.finishLock.threadFinished()方法:
private synchronized void threadFinished() {if (this.threadsToFinish == WindowsSelectorImpl.this.threads.size()) {// 如果它是第一个返回的线程,则执行wakeup()WindowsSelectorImpl.this.wakeup();}// 将待完成的线程数量-1--this.threadsToFinish;if (this.threadsToFinish == 0) {// 如果线程全部返回,则唤醒主线程this.notify();}}
可以看到如果多个辅助线程都有返回,实际上 WindowsSelectorImpl.this.wakeup(); 方法也只调用了一次,然后将待完成的线程数量减1,如果都完成,则唤醒主线程绩效向下处理;
看wakeup()方法:
public Selector wakeup() {synchronized(this.interruptLock) {if (!this.interruptTriggered) {this.setWakeupSocket();this.interruptTriggered = true;}return this;}
}
首先判断 interruptTriggered,如果为 True,立即返回;如果为 False,调用 setWakeupSocket (), 并将 interruptTriggered 设置为 true;
下面看 setWakeupSocket () 的实现:
private void setWakeupSocket() {this.setWakeupSocket0(this.wakeupSinkFd);
}private native void setWakeupSocket0(int var1);
传入管道 sink 端的 wakeupSinkFd,然后调用底层的 setWakeupSocket0 方法,下面从 openjdk8 源文件 WindowsSelectorImpl.c 找到 setWakeupSocket0 的实现:
Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this,jint scoutFd)
{/* Write one byte into the pipe */const char byte = 1;send(scoutFd, &byte, 1, 0);
}
该函数的主要作用是向 pipe 的 sink 端写入了一个字节,这样 pipe 的 source 端文件描述符立即就会处于就绪状态,select () 方法将立即从阻塞中返回,这样就完成了唤醒 selector 的功能;
wakeup () 中使用 interruptTriggered 来判断是否执行唤醒操作。因此,在 select 期间,多次调用 wakeup () 产生的效果与调用一次是一样的,因为后面的调用将不会满足唤醒条件。
如果调用 wakeup () 期间没有 select 操作,当调用 wakeup () 之后,interruptTriggered 被设置为 true,pipe 的 source 端 wakeupSourceFd 就会处于就绪状态。如果此时调用 select 相关操作时,会调用 resetWakeupSocket 方法,resetWakeupSocket 首先会调用本地方法 resetWakeupSocket0 读取 wakeup () 中发送的数据,再将 interruptTriggered 设置为 false,最后 doSelect 将会立即返回 0,而不会调用 poll 操作。
这里如何实现将所有线程的poll() 阻塞都进行唤醒呢:
实例化 WindowsSelectorImpl 时,选择器会将 wakeupSourceFd 加入 pollWrapper,这正是用于实现唤醒功能。
WindowsSelectorImpl(SelectorProvider var1) throws IOException {super(var1);this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();var2.sc.socket().setTcpNoDelay(true);this.wakeupSinkFd = var2.getFDVal();// 增加唤醒描述符this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
}
当 Channel 注册到选择器上时,如果满足需要增加辅助线程的条件,选择器会再次将 wakeupSourceFd 加入 pollWrapper。
private void growIfNeeded() {if (this.channelArray.length == this.totalChannels) {int var1 = this.totalChannels * 2;SelectionKeyImpl[] var2 = new SelectionKeyImpl[var1];System.arraycopy(this.channelArray, 1, var2, 1, this.totalChannels - 1);this.channelArray = var2;this.pollWrapper.grow(var1);}if (this.totalChannels % 1024 == 0) {// 增加唤醒描述符this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, this.totalChannels);++this.totalChannels;++this.threadsCount;}}
这样进行分组之后,每个线程监听的 Fd 列表第一个都为 wakeupSourceFd。在调用 wakeup () 执行唤醒操作时,所有线程都能监听到 wakeupSourceFd 上有就绪事件发生,这就实现了唤醒所有阻塞在 poll 调用的线程;
当辅助线程被唤醒调用WindowsSelectorImpl.this.finishLock.threadFinished() 之后,进行进行下一次的for 循环,此时this.runsCounter == var1.lastRun 条件有一次满足则当前线程又被wait 方法挂起,等待下一次select 判断当前线程是否需要增加或者减少,如果增加了线程则在线程启动后先统一被wait 挂起,等待后续被主线程统一唤醒;如果该线程多余则var1.isZombie() 会返回true ,则当前线程直接跳出for 循环;
private synchronized boolean waitForStart(WindowsSelectorImpl.SelectThread var1) {while(this.runsCounter == var1.lastRun) {try {WindowsSelectorImpl.this.startLock.wait();} catch (InterruptedException var3) {Thread.currentThread().interrupt();}}if (var1.isZombie()) {return true;} else {var1.lastRun = this.runsCounter;return false;}}
在看第二种主线程中监听的socket 有事件返回:
try {this.begin();try {this.subSelector.poll();// 主线程从poll 阻塞返回} catch (IOException var7) {this.finishLock.setException(var7);}// 判断本次select 是否动用了子线程if (this.threads.size() > 0) {// 判断是否所有的子线程都进行了返回this.finishLock.waitForHelperThreads();}
} finally {this.end();
}
在看 this.finishLock.waitForHelperThreads():
private synchronized void waitForHelperThreads() {if (this.threadsToFinish == WindowsSelectorImpl.this.threads.size()) {WindowsSelectorImpl.this.wakeup();}while(this.threadsToFinish != 0) {// 当发现还有子线程的poll 没有返回则 将主线程 waittry {WindowsSelectorImpl.this.finishLock.wait();} catch (InterruptedException var2) {Thread.currentThread().interrupt();}}}
可以看到主线程如果发现还有子线程的poll没有返回这里会将在通过wait方法挂起,当子线程都返回的时候通过子线程 的notify 方法唤醒主线程
private synchronized void threadFinished() {if (this.threadsToFinish == WindowsSelectorImpl.this.threads.size()) {WindowsSelectorImpl.this.wakeup();}--this.threadsToFinish;if (this.threadsToFinish == 0) {// 这里唤醒主线程this.notify();}}
每次在发起系统调用之前,都首先会调用 finishLock 的 reset () 重置 threadsToFinish 为当前辅助线程的数量。当第一个线程从系统调用 poll 中返回时,由该线程负责唤醒其他正在阻塞等待的线程。任何一个辅助线程从系统调用 poll 中返回,都会调用 threadFinished (),将 threadsToFinish 减 1。
当 threadsToFinish 为 0 时,调用 notify () 唤醒处于等待中的线程。那么通常谁会处于等待状态呢?答案是主线程,当主线程从系统调用 poll 中返回时,会调用 waitForHelperThreads (),如果此时 threadsToFinish 不为 0,说明还有辅助线程没有从系统调用 poll 中返回,主线程将进入等待状态。
WindowsSelectorImpl 通过使用 finishLock 来实现了协调所有线程同时从内核调用中返回,向客户端屏蔽了多线程执行系统调用 poll 的细节,让每次 select 调用都像只由主线程完成一样。
poll 系统调用:
内部类 SubSelector 封装了系统调用 poll 操作,并负责调用 poll0 () 向系统内核发起查询。看源码:
private int poll() throws IOException{ // 主线程调用return poll0(pollWrapper.pollArrayAddress,Math.min(totalChannels, MAX_SELECTABLE_FDS),readFds, writeFds, exceptFds, timeout);
}private int poll(int index) throws IOException {// 辅助线程调用return poll0(pollWrapper.pollArrayAddress +(pollArrayIndex * PollArrayWrapper.SIZE_POLLFD),Math.min(MAX_SELECTABLE_FDS,totalChannels - (index + 1) * MAX_SELECTABLE_FDS),readFds, writeFds, exceptFds, timeout);
}private native int poll0(long pollAddress, int numfds,int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
poll0 () 参数介绍:
- pollAddress:FD 数组内存起始地址;
- numfds:待监听的 FD 数量;
- readFds:用于接收发生可读事件的 FD;
- writeFds:用于接收发生可写事件的 FD;
- exceptFds:用于接收发生异常的 FD;
- timeout:超时等待时间。
由于每个线程最大会处理 1024 个通道(包含唤醒通道),因此 readFds,writeFds,exceptFds 数组的长度均为 1025。其中 readFds [0] 为实际发生可读事件的 FD 数量,即 poll 完成之后 readFds 的实际长度,writeFds,exceptFds 同理;
当本次select 从poll 获取事件返回后,先判断poll 过程中是否有异常:
this.finishLock.checkForException();
private void checkForException() throws IOException {if (this.exception != null) {StringBuffer var1 = new StringBuffer("An exception occurred during the execution of select(): \n");var1.append(this.exception);var1.append('\n');this.exception = null;throw new IOException(var1.toString());}
}
然后清除无效的通道, this.processDeregisterQueue();:
// 先决条件: 在this, keys, and selectedKeys上加锁
void processDeregisterQueue() throws IOException {// 取消的SelectionKey 集合Set var1 = this.cancelledKeys();synchronized(var1) {if (!var1.isEmpty()) {Iterator var3 = var1.iterator();while(var3.hasNext()) {SelectionKeyImpl var4 = (SelectionKeyImpl)var3.next();try {this.implDereg(var4);} catch (SocketException var11) {throw new IOException("Error deregistering key", var11);} finally {var3.remove();}}}}
}
首先对 cancelledKeys 加锁,防止其他线程在此期间调用 cancel () 方法向 cancelledKeys 集合中添加选择键。如果 cancelledKeys 集合非空,迭代 cancelledKeys 集合,调用 implDereg () 进行注销,并从 cancelledKeys 中移除选择键。
implDereg :
protected void implDereg(SelectionKeyImpl var1) throws IOException {// 取消的selectedKey 下标int var2 = var1.getIndex();assert var2 >= 0;// 如果选择键的索引不是选择键数组 channelArray 最后一个元素。// 需要将最后一个元素 endChannel 放到待注销选择键的位置,并更新其索引为待注销选择键的索引,// 同时需要将 pollWrapper 中最后一个元素替换到待注销 FD 的位置// 将selectedKey 下标置为-1synchronized(this.closeLock) {if (var2 != this.totalChannels - 1) {SelectionKeyImpl var4 = this.channelArray[this.totalChannels - 1];this.channelArray[var2] = var4;var4.setIndex(var2);this.pollWrapper.replaceEntry(this.pollWrapper, this.totalChannels - 1, this.pollWrapper, var2);}var1.setIndex(-1);}// channelArray 的最后一个元素引用置为空this.channelArray[this.totalChannels - 1] = null;// 通道总数量 totalChannels 减 1--this.totalChannels;// 如果 totalChannels 不为 1 且 totalChannels % MAX_SELECTABLE_FDS 为 1// 说明 channelArray 中该位置没有放置元素(加入唤醒通道时会跳过该位置)if (this.totalChannels != 1 && this.totalChannels % 1024 == 1) {// 需要将 totalChannels 和辅助线程数量 threadsCount 减 1--this.totalChannels;--this.threadsCount;}// 从 fdMap,keys 和 selectedKeys 中移除当前选择键;this.fdMap.remove(var1);this.keys.remove(var1);this.selectedKeys.remove(var1);// 调用 deregister 从通道的键集合中注销该选择键this.deregister(var1);// 如果选择键对应的通道已经关闭并且没有注册到其他选择器上,调用 kill () 关闭通道SelectableChannel var3 = var1.channel();if (!var3.isOpen() && !var3.isRegistered()) {((SelChImpl)var3).kill();}}
使用内部的 cancelledKeys 集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化。注销通道是一个潜在的代价很高的操作,这可能需要重新分配资源(请记住,键是与通道相关的,并且可能与它们相关的通道对象之间有复杂的交互)。清理已取消的键,并在选择操作之前和之后立即注销通道,可以消除它们可能正好在选择的过程中执行的潜在棘手问题。这是另一个兼顾健壮性的折中方案。
this.updateSelectedKeys() 更新已选择队列:
updateSelectedKeys 负责处理发生就绪事件的 FD,将这些 FD 对应的选择键加入 selectedKeys 集合。客户端通过遍历 selectedKeys 集合即可处理各种事件:
private int updateSelectedKeys() {// 更新数量+1++this.updateCount;byte var1 = 0;// 处理主线程上的发生就绪事件的 FD 列表int var4 = var1 + this.subSelector.processSelectedKeys(this.updateCount);// 迭代 threads 集合分别处理每个辅助线程上发生就绪事件的 FD 列表WindowsSelectorImpl.SelectThread var3;for(Iterator var2 = this.threads.iterator(); var2.hasNext(); var4 += var3.subSelector.processSelectedKeys(this.updateCount)) {var3 = (WindowsSelectorImpl.SelectThread)var2.next();}return var4;}
处理辅助线程事件,subSelector.processSelectedKeys:
private int processSelectedKeys(long var1) {byte var3 = 0;int var4 = var3 + this.processFDSet(var1, this.readFds, Net.POLLIN, false);var4 += this.processFDSet(var1, this.writeFds, Net.POLLCONN | Net.POLLOUT, false);var4 += this.processFDSet(var1, this.exceptFds, Net.POLLIN | Net.POLLCONN | Net.POLLOUT, true);return var4;
}
分别处理 readFds,writeFds,exceptFds 三个数组中的 FD;
processFDSet处理过程:
private int processFDSet(long var1, int[] var3, int var4, boolean var5) {int var6 = 0;for(int var7 = 1; var7 <= var3[0]; ++var7) {int var8 = var3[var7];if (var8 == WindowsSelectorImpl.this.wakeupSourceFd) {synchronized(WindowsSelectorImpl.this.interruptLock) {// processFDSet 负责轮询 FD 数组,并处理每个 FD。如果 FD 为 wakeupSourceFd,// 只需将 interruptTriggered 置为 true;WindowsSelectorImpl.this.interruptTriggered = true;}} else {WindowsSelectorImpl.MapEntry var9 = WindowsSelectorImpl.this.fdMap.get(var8);if (var9 != null) {SelectionKeyImpl var10 = var9.ski;// readFds,writeFds 的var5 都是false,exceptFds 的var5 是true// 如果是exceptFds ,如果选择键对应的通道类型不是 SocketChannelImpl,// 通常为 ServerSocketChannelImpl,则判断条件不成立// 如果选择键对应的通道类型是 SocketChannelImpl,调用 discardUrgentData // 判断是否忽略客户端 socket 发送的 OOB 数据(带外数据),如果不忽略,条件不成立// windows 环境下,客户端 socket 通常使用 sendUrgentData 发送紧急数据(类似于心跳包)用于检测连接的有效性if (!var5 || !(var10.channel() instanceof SocketChannelImpl) || !WindowsSelectorImpl.this.discardUrgentData(var8)) {// 如果是读或者写描述符则直接进入;// 如果是服务端的管道也直接进入// 如果是客户端的通道,判断是否需要忽略客户端的心跳包,如果不忽略则进入if (WindowsSelectorImpl.this.selectedKeys.contains(var10)) {// 如果事件已经在selectedKeys if (var9.clearedCount != var1) {if (var10.channel.translateAndSetReadyOps(var4, var10) && var9.updateCount != var1) {var9.updateCount = var1;++var6;}} else if (var10.channel.translateAndUpdateReadyOps(var4, var10) && var9.updateCount != var1) {var9.updateCount = var1;++var6;}var9.clearedCount = var1;} else {// 如果事件不在selectedKeys 中,进行添加操作if (var9.clearedCount != var1) {var10.channel.translateAndSetReadyOps(var4, var10);if ((var10.nioReadyOps() & var10.nioInterestOps()) != 0) {WindowsSelectorImpl.this.selectedKeys.add(var10);var9.updateCount = var1;++var6;}} else {var10.channel.translateAndUpdateReadyOps(var4, var10);if ((var10.nioReadyOps() & var10.nioInterestOps()) != 0) {WindowsSelectorImpl.this.selectedKeys.add(var10);var9.updateCount = var1;++var6;}}var9.clearedCount = var1;}}}}}return var6;
}
processFDSet 中,如果通道的键还没有处于已选择的键的集合中,那么键的 ready 集合将被清空,然后 poll 操作发现的当前通道已经准备好的事件的比特掩码将被设置;否则,通道的键已经处于已选择的键的集合中,键的 ready 集合将被 poll 操作发现的当前已经准备好的事件的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。新就绪的事件集是与之前的 ready 集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的 ready 集合将是累积的。比特位只会被设置,不会被清理。
select 操作返回的值是 ready 集合在 processFDSet 中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个 select ( ) 调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是 0。
总结:
1)selector.select() 过程中为了提高效率,如果通道超过1024 会增加辅助线程,增加效率;通过WindowsSelectorImpl.StartLock startLock 控制辅助线程,监听各自的通道描述符事件;当通道有事件发生,通过wakeup 唤醒所有处于poll阻塞的线程;通过 WindowsSelectorImpl.FinishLock finishLock 来协调所有线程的返回;
2)当poll 完成后主线程处理所有线程监听的描述符,将新到的已经就绪的通道事件加入到,SelectorImpl selectedKeys 中供后续轮询判断连接/可读/可写事件是否准备就绪;
参考:
1 Selector 源码深入分析之 Window 实现(上篇);
2 Selector 源码深入分析之 Window 实现(下篇);
3 Java NIO Selector 实现原理;
4 Java NIO wakeup 实现原理;