前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Tomcat NIO(14)-BlockPoller线程的阻塞与唤醒

Tomcat NIO(14)-BlockPoller线程的阻塞与唤醒

作者头像
TA码字
发布2020-11-19 17:44:40
发布2020-11-19 17:44:40
96700
代码可运行
举报
文章被收录于专栏:TA码字TA码字
运行总次数:0
代码可运行

上一篇文章我们主要介绍了 tomcat NIO 之中的 block poller 线程,包括启动 block poller 线程,添加事件到队列,对原始 socket 注册事件和 block poller 线程的核心逻辑。这里我们主要介绍 block poller 线程的阻塞与唤醒。

根据以前文章,block poller 线程一般会和 tomcat io 线程有交互,即 io 线程会把事件放到 block poller 线程的 SynchronizedQueue 事件队列之中。而 block poller 线程会轮询事件队列进行操作,但是不能一直 while(true) 的轮询,这样会占用大量的 cpu 资源,所以会有 block poller 线程的阻塞与唤醒(一般由tomcat io线程注册事件的时候唤醒)。对于该设计,主要包括以下:

  • 关键对象和实例
  • block poller 线程的阻塞
  • block poller 线程的唤醒

关键对象和实例

block poller 线程的阻塞与唤醒主要涉及 block poller 实例的 selector 属性和 wakeupCounter(AtomicLong类型)属性。

  • 上一篇文章中 block poller 的核心逻辑会调用 selector.selectNow() 方法来获取是否有注册在原始 socket 上的事件发生。这个方法是非阻塞方法,即调用之后立即返回,不会阻塞当前 block poller 线程,这个方法会在确定有事件添加到队列的情况下调用,这样尽可能监测到连接是否有可读或可写事件。
  • block poller 调用 selector.select(timeout) 方法,这个方法是阻塞方法,调用该方法之后 block poller 线程会一直处于等待状态,一直等待到有事件发生或者超时。这个方法会在没有事件添加到队列的情况下调用,从而让 block poller 线程进入等待状态,避免 cpu 空闲轮询造成使用率过高(极端情况下会导致 java 进程占用 cpu 100% 的现象)。
  • block poller 实例会有 wakeupCounter 属性,这个属性为 AtomicLong 类型,初始值为 0,在 tomcat io 线程注册事件的时候,会根据该值是否为 0 来决定是否由 io 线程唤醒 block poller 线程。

BlockPoller线程的阻塞

该线程的阻塞由 block poller 的 run() 方法实现,主要核心逻辑如下:

代码语言:javascript
代码运行次数:0
复制
private AtomicLong wakeupCounter = new AtomicLong(0);
//Run method in BlockPoller class
int i = wakeupCounter.get();
if (i > 0) {
    keyCount = selector.selectNow();
} else {
    wakeupCounter.set(-1);
    keyCount = selector.select(1000);
}
wakeupCounter.set(0);
  • wakeupCounter 初始值为0 ,它的 get() 方法调用返回值依然为原始 0,所以逻辑进入到 else 分支中。
  • 在 else 分支中将 wakeupCounter 设为 -1 ,同时 selector.select(timeout) 被调用,因为没有为队列中的原始 socket 注册可读可写事件,所以 block poller 线程会阻塞,放弃对 cpu 的使用,一直到超时。
  • wakeupCounter.set(0) 被调用,将指设置回原始 0。后面如果还是没有对原始 socket 注册可读可写事件,依然循环上面的步骤。
  • selectorTimeout 的默认值为 1000 毫秒,即会阻塞 block poller 线程 1 秒钟,然后进入下一个循环。

BlockPoller线程的唤醒

block poller 线程的唤醒由该实例的 add() 方法实现,根据以前文章,其间接的被tomcat io 线程在请求体不可读或者响应数据不可写的时候调用,所以是 tomcat io线程完成对 block poller 线程的唤醒,其核心逻辑如下:

代码语言:javascript
代码运行次数:0
复制
public void add(final NioSocketWrapper key, final int ops, final KeyReference ref) {
    if (key == null) {
        return;
    }
    NioChannel nch = key.getSocket();
    final SocketChannel ch = nch.getIOChannel();
    if (ch == null) {
        return;
    }
    Runnable r = new RunnableAdd(ch, key, ops, ref);
    events.offer(r);
    wakeup();
}

public void wakeup() {
    if (wakeupCounter.addAndGet(1)==0){
        selector.wakeup();
    }
}
  • 对于 tomcat io 线程: 1. 间接调用该方法把事件放入 block poller 队列之中,并且调用 wakeupCounter.addAndGet(1) 方法。 2. 根据上面"block poller线程的阻塞"部分的分析,当线程阻塞的时候,wakeupCounter 的值为 -1 。这里通过调用 addAndGet(1) 方法加 1,使其值变为 0,然后调用 selector.wakeup() 唤醒处于阻塞状态的 block poller 线程。
  • 对于 block poller 线程: 1. 其被 tomcat io 线程唤醒之后继续执行 run() 方法,run() 方法会间接调用 events() 方法。根据上一篇文章,event() 方法会对事件队列中关联的所有原始 socket 对象注册读写事件。这个时候就可能有可读可写事件发生,后面通过 select.selectNow() 或 selector.select(timeout) 来监听。 2. 如果 tomcat io 线程并发多次把事件放入队列,那么 wakeupCounter 的值一定会大于 0 ,wakeupCounter.get() 的返回值也大于 0 ,这时block poller 线程逻辑就会在循环里调用 select.selectNow() 非阻塞方法来检查可读可写事件是否发生。 3. 对于 block poller 来说,如果被唤醒了,调用 selector.selectNow()/selector.select(timeout) 也不一定获得事件。因为注册的是可读或者可写事件,可读的发生还是靠 client 端把数据发送过来,可写的发生是要求原始 socket 缓冲区可用。之所以要唤醒 block poller 线程,是因为对原始 socket 的读写事件在 events() 方法里注册好了。正常情况下,可读在 client 端在建立好连接之后应该会发送数据发生,可写在发送完上一次的响应数据之后原始 socket 缓冲区就可用。所以就有数据可读可写的可能性,然后马上唤醒 poller 线程,来用 selector 监测是否有可读可写事件发生。

Tomcat 正是通过以上 block poller 线程的阻塞与唤醒的设计,最大程度的避免了该线程对 cpu 的占用,同时又在对原始 socket 注册读写事件之后唤醒 block poller 线程去监测数据的可读可写性。其实这里的设计思路和以前文章中介绍的 poller 线程的阻塞与唤醒设计思路一样,目前先写到这里,下一篇文章里我们继续介绍 tomcat 的长连接。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 TA码字 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档