前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Tomcat NIO(8)-Poller线程的阻塞与唤醒

Tomcat NIO(8)-Poller线程的阻塞与唤醒

作者头像
TA码字
发布2020-09-10 15:11:28
1.3K0
发布2020-09-10 15:11:28
举报
文章被收录于专栏:TA码字TA码字

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

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

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

关键对象和实例

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

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

Poller线程的阻塞

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

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

Poller线程的唤醒

poller 线程的唤醒由 poller 实例的 addEvent() 方法实现,根据以前文章,其间接的被 acceptor 线程通过 poller.register() 调用 trigger 的,所以是 acceptor 线程完成对 poller 线程的唤醒,其核心逻辑如下:

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

Tomcat 正是通过以上 poller 线程的阻塞与唤醒的设计,最大程度的避免了 poller 线程对 cpu 的占用,同时又在有 client 连接 ready 的时候唤醒 poller 线程去监测 client 连接数据的可读性。目前先写到这里,下一篇文章里我们继续介绍 tomcat io 线程。

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

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

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

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

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