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

上一篇文章我们主要介绍了 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() 方法实现,主要核心逻辑如下:

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 线程的唤醒,其核心逻辑如下:

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 的长连接。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值