Spring架构篇--2.5.3 远程通信基础Select 源码篇--window-- selector.select()

news/2024/4/25 15:16:07/文章来源:https://blog.csdn.net/l123lgx/article/details/129139053

前言: 通过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 实现原理;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_72473.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

电子技术——负反馈特性

电子技术——负反馈特性 本节我们进一步深入介绍负反馈特性。 增益脱敏性 假设 β\betaβ 是一个常数。考虑下面的微分方程&#xff1a; dAfdA(1Aβ)2dA_f \frac{dA}{(1 A\beta)^2} dAf​(1Aβ)2dA​ 将上式除以 AfA1AβA_f \frac{A}{1A\beta}Af​1AβA​ 得到&#xff1…

php+vue加油站会员服务系统 java微信小程序

目 录 1绪论 1 1.1项目研究的背景 1 1.2开发意义 1 1.3项目研究现状及内容 5 1.4论文结构 5 2开发技术介绍 7 2.5微信小程序技术 8 3系统分析 9 3.1可行性分析 9 3.1.1技术可行性 9 3.1.2经济可行性 9 3.1.3操作可行性 10 3.2网站性能需求分析 10 3.3网站功能分析 10 3.4系统…

秒懂算法 | 子集树模型——0-1背包问题的回溯算法及动态规划改进

给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为W。一种物品要么全部装入背包,要么全部不装入背包,不允许部分装入。装入背包的物品的总重量不超过背包的容量。问应如何选择装入背包的物品,使得装入背包中的物品总价值最大? 01、问题分析——解空间及搜索…

JAVA并发集合之ConcurrentHashMap

ConcurrentHashMap是一个支持高并发更新与查询的哈希表(基于HashMap)。Hashmap在多线程并发的情况下&#xff0c;是线程不安全的&#xff0c;容易出现死循环、死锁等问题&#xff0c;JDK8后不会出现死锁问题&#xff0c;但依然存在多线程的系列问题&#xff0c;如&#xff1a;数…

关于虚拟数字人你想知道的都在这里

2022年底&#xff0c;微软旗下的人工智能实验室Open AI发布的对话式大型语言模型ChatGPT聊天机器人一夜蹿红&#xff0c;5天用户量超百万&#xff0c;在各大中外媒体平台掀起了一阵热潮。也带火了人工智能相关产业&#xff0c;AI虚拟数字人就是其中之一&#xff0c;一个随着元宇…

【电商】红冲与单价调整

产品及系统的规划与设计过程中始终会考虑实际生产环境中的异常场景&#xff0c;这样会增加系统复杂度&#xff0c;虽然有时可以通过简化流程来降低出现异常的概率&#xff0c;但很多时候都是无法避开的&#xff1b;本篇就简单梳理下红冲单与价格调整单方面的内容&#xff0c;希…

Java ”框架 = 注解 + 反射 + 设计模式“ 之 注解详解

Java ”框架 注解 反射 设计模式“ 之 注解详解 每博一文案 刹那间我真想令时光停住&#xff0c;好让我回顾自己&#xff0c;回顾失去的年华&#xff0c;缅怀哪个穿一身短小的连衣裙 和瘦窄的短衫的小女孩。让我追悔少年时代&#xff0c;我心灵的愚钝无知&#xff0c;它轻易…

解决 NestHost requires ASM7 (shrink、kotlin metadata)

① 场景 Caused by: java.lang.RuntimeException: NestHost requires ASM7Failed to resolve class org/vigame/demo/CrashHandler$1.class[transform input:not foundproject input:not foundaar input:not found]Caused by: java.lang.UnsupportedOperationException: NestH…

损失函数与反向传播

一、损失函数计算实际输出和目标之间的差距为我们更新输出提供一定的依据&#xff08;反向传播&#xff09;1.nn.L1Lossimport torch from torch.nn import L1Loss inputs torch.tensor([1,2,3],dtypetorch.float) targets torch.tensor([1,2,5],dtypetorch.float) # reshape…

ZED相机快速使用指南

1、安装SDK ZED SDK 3.8 - Download | Stereolabs 2、安装ros GitHub - stereolabs/zed-ros-wrapper: ROS wrapper for the ZED SDK 其他教程&#xff1a;ZED2相机SDK安装使用及ROS下使用_可即的博客-CSDN博客 3、官方文档 Get Started with ZED | Stereolabs 4、标定参…

「TCG 规范解读」第12章 TPM工作组 TCG身份验证研讨

可信计算组织&#xff08;Ttrusted Computing Group,TCG&#xff09;是一个非盈利的工业标准组织&#xff0c;它的宗旨是加强在相异计算机平台上的计算环境的安全性。TCG于2003年春成立&#xff0c;并采纳了由可信计算平台联盟&#xff08;the Trusted Computing Platform Alli…

游戏蓝牙耳机哪个品牌好?游戏蓝牙耳机品牌排行榜

手机端的TWS耳机已成为主流&#xff0c;因而许多厂商也在制造蓝牙耳机时&#xff0c;不仅仅只限于音质&#xff0c;并且在延迟和功能上有所改进&#xff0c;下面小编整理了游戏蓝牙耳机品牌排行榜&#xff0c;看看有哪些入围的吧&#xff01; 一、南卡小音舱蓝牙耳机 蓝牙版本…

华为OD机试 - 计算网络信号(C++) | 附带编码思路 【2023】

刷算法题之前必看 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看地址:https://blog.csdn.net/hihell/category_12199283.html 华为OD详细说明:https://dream.blog.csdn.net/article/details/128980730 华为OD机试题…

左耳听风——笔记四:分布式

左耳听风&#xff1a;分布式 分布式系统介绍 分布式系统和单体系统 使用分布式系统主要有两方面原因。 增大系统容量。我们的业务量越来越大&#xff0c;而要能应对越来越大的业务量&#xff0c;一台机器的性能已经无法满足了&#xff0c;我们需要多台机器才能应对大规模的…

吐血整理AutoSAR Com-Stack 的配置【基于ETAS】

总目录链接>> AutoSAR入门和实战系列总目录 文章目录01.软件组件和系统说明02.基本软件配置03.系统数据映射04.代码生成05.代码整合06.测试下图显示了基于 AUTOSAR 的 ECU SW 的结构。纵观BSW&#xff0c;大体分为三层。三层模块中&#xff0c;与通信相关的模块称为通信…

精确控制 AI 图像生成的破冰方案,ControlNet 和 T2I-Adapter

ControlNet 和 T2I-Adapter 的突破性在哪里&#xff1f;有什么区别&#xff1f;其它为 T2I 扩散模型施加条件引导的相关研究ControlNet 和 T2I-Adapter 的实际应用效果如何&#xff1f;使用体验上&#xff0c;跟 SD原生支持的 img2img 有什么区别&#xff1f;ControlNet 在插画…

运动戴耳机哪种款式比较好、最好用的运动耳机

很多人喜欢运动时听音乐,因为在运动场景中,听歌的节奏与步频匹配的时候&#xff0c;的确是可以起到很好的激励和缓解情绪的作用。认认真真地选择一副适合自己跑步的运动耳机&#xff0c;成了很多跑步爱好者的实际需求&#xff0c;专门为运动打造的耳机也不少!那么,如何挑选一款…

网络信息安全(四)

IIS WEB服务器 服务器配置静态IP 安装WEB服务软件 打开软件 检查80端口是否打开 DNS解析不同域名站点 新建两个网页京东和淘宝 安装DNS组件并创建两个区域 新建主机 XP上指定DNS 正常情况下同一个服务器上一个端口只提供一个服务 添加主机头值 XP验证 IIS FTP服务器 FTP工作模式…

0基础学插画是报班还是自学

学插画0基础是报班还是自学&#xff0c;众所周知&#xff0c;报班一定是提升插画水平的最有效途径&#xff0c;如果有经济能力&#xff0c;建议报班&#xff01;那么报哪些插画课程班比较靠谱呢&#xff1f;同时给大家梳理了国内最新5大插画班排行榜&#xff0c;各有优势和特色…

解析Java中的class文件

解析class文件需要把class文件当成文件流来处理&#xff0c;定义ClassReader结构体 type ClassReader struct {data []byte }go语言中的reslice语法可以跳过已经读过的数据。 同时定义了ClassFile数据结构来描述class文件的各个部分&#xff0c;该数据结构如下所示&#xff1…