前言

这两年,tomcat慢慢在新项目里不怎么接触了,因为都被spring boot之类的框架封装进了内部,成了内置server,不用像过去那样打个war包,再放到tomcat里部署了。

但是,内部的机制我们还是有必要了解的,尤其是线程模型和classloader,这篇我们会聚焦线程模型。

其实我本打算将一个问题,即大家知道,我们平时最终写的controller、service那些业务代码,最终是由什么线程来执行的呢?

大家都是debug过的人,肯定知道,线程名称大概如下:

  1. http-nio-8080-exec-2@5076

这个线程是tomcat的线程,假设,我们在这个线程里,sleep个1分钟,模拟调用第三方服务时,第三方服务异常卡住不返回的情况,此时客户端每秒100个请求过来,此时整个程序会出现什么情况?

但是我发现,这个问题,一篇还是讲不太清楚,因此,本篇只讲一下线程模型。

主要线程模型简介

大家可以思考下,一个服务端程序,有哪些是肯定需要的?

我们肯定需要开启监听对吧,大家看看下面的bio程序:

这个就是个线程,在while(true)死循环里,一直accept客户端连接。

ok,这个线程肯定是需要的。接下来,再看看还是否需要其他的线程。

如果一切从简,我们只用这1个线程也足够了,就像redis一样,redis都是内存操作,做啥都很快,还避免了线程切换的开销;

但是我们的java后端,一般都要操作数据库的,这个是比较慢,自然是希望把这部分工作能够交给单独的线程去做,在tomcat里,确实是这样的,交给了一个线程池,线程池里的线程,就是我们平时看到的,名称类似http-nio-8080-exec-2@5076这样的,一般默认配置,最大200个线程。

但如果这样的话,1个acceptor + 一个业务线程池,会导致一个问题,就是,该acceptor既要负责新连接的接入,还要负责已接入连接的socket的io读写。假设我们维护了10万个连接,这10万个连接都在不断地给我们的服务端发数据,我们服务端也在不停地给客户端返回数据,那这个工作还是很繁重的,可能会压垮这个唯一的acceptor线程。

因此,理想情况下,我们会在单独弄几个线程出来,负责已经接入的连接的io读写。

大体流程:

  1. acceptor--->poller线程(负责已接入连接的io读写)-->业务线程池(http-nio-8080-exec-2@5076)

这个大概就是tomcat中的流程了。

在netty中,其实是类似的:

  1. boss eventloop--->worker eventloop-->一般在解码完成后的最后一个handler,交给自定义业务线程池

tomcat如何接入新连接

大家可以看看下图,这里面有几个橙色的方块,这几个代表了线程,从左到右,分别就是acceptor、nio线程池、poller线程。

  • 1处,acceptor线程内部维护了一个endpoint对象,这个对象呢,就代表了1个服务端端点;该对象有几个实现类,如下:

    我们spring boot程序里,默认是用的NioEndpoint。

  • 2处,将新连接交给NioEndpoint处理

    1. @Override
    2. protected boolean setSocketOptions(SocketChannel socket) {
    3. // Process the connection
    4. try {
    5. // Disable blocking, polling will be used
    6. socket.configureBlocking(false);
    7. Socket sock = socket.socket();
    8. socketProperties.setProperties(sock);
    9. // 进行一些socket的参数设置
    10. NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this);
    11. channel.setSocketWrapper(socketWrapper);
    12. socketWrapper.setReadTimeout(getConnectionTimeout());
    13. socketWrapper.setWriteTimeout(getConnectionTimeout());
    14. //3 交给poller处理
    15. poller.register(channel, socketWrapper);
    16. return true;
    17. }
    18. ...
    19. // Tell to close the socket
    20. return false;
    21. }
  • 3处,就是交给NioEndpoint内部的poller对象去进行处理。

    1. public void register(final NioChannel socket, final NioSocketWrapper socketWrapper) {
    2. socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
    3. PollerEvent r = null;
    4. // 丢到poller的队列里,poller线程会轮旋该队列
    5. r = new PollerEvent(socket, OP_REGISTER);
    6. // 丢到队列里
    7. addEvent(r);
    8. }

    上面的addEvent值得一看。

    1. private final SynchronizedQueue<PollerEvent> events =
    2. new SynchronizedQueue<>();
    3. private void addEvent(PollerEvent event) {
    4. // 丢到队列里
    5. events.offer(event);
    6. // 唤醒poller里的selector,及时将该socket注册到selector中
    7. if (wakeupCounter.incrementAndGet() == 0) {
    8. selector.wakeup();
    9. }
    10. }

    到这里,acceptor线程的逻辑就结束了,一个异步放队列,完美收工。接下来,就是poller线程的工作了。

    poller线程,要负责将该socket注册到selector里面去,然后还要负责该socket的io读写事件处理。

  • poller线程逻辑

    1. public class Poller implements Runnable {
    2. private Selector selector;
    3. private final SynchronizedQueue<PollerEvent> events =
    4. new SynchronizedQueue<>();

    可以看到,poller内部维护了一个selector,和一个队列,队列里也说了,主要是要新注册到selector的新socket。

    既然丢到队列了,那我们看看什么时候去队列取的呢?

    1. @Override
    2. public void run() {
    3. // Loop until destroy() is called
    4. while (true) {
    5. boolean hasEvents = false;
    6. // 检查events
    7. hasEvents = events();
    8. }
    9. }

    这里我们跟一下events()。

    1. public boolean events() {
    2. boolean result = false;
    3. PollerEvent pe = null;
    4. for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
    5. result = true;
    6. pe.run();
    7. ...
    8. }
    9. return result;
    10. }

    这里的

    1. pe = events.poll()

    就是去队列拉取事件,拉取到了之后,就会赋值给pe,然后下面就调用了pe.run方法。

    pe的类型是PollerEvent,我们看看其run方法会干啥?

    1. @Override
    2. public void run() {
    3. if (interestOps == OP_REGISTER) {
    4. try { socket.getIOChannel().register(socket.getSocketWrapper().getPoller().getSelector(), SelectionKey.OP_READ, socket.getSocketWrapper());
    5. } catch (Exception x) {
    6. log.error(sm.getString("endpoint.nio.registerFail"), x);
    7. }
    8. }
    9. }

    这个方法难理解吗,看着有点吓人,其实就是把这个新的连接,向selector注册,感兴趣的io事件为OP_READ。后续呢,这个连接的io读写,就全由本poller的selector包了。

tomcat如何处理客户端读事件

我们说了,poller是个线程,在其runnable实现里,除了要处理上面的新连接注册到selector这个事,还要负责io读写,这部分逻辑就是在:

  1. Iterator<SelectionKey> iterator=selector.selectedKeys().iterator();
  2. while (iterator != null && iterator.hasNext()) {
  3. SelectionKey sk = iterator.next();
  4. NioSocketWrapper socketWrapper = sk.attachment();
  5. processKey(sk, socketWrapper);
  6. }

最后一行的processKey,会调用如下逻辑,将工作甩锅给http-nio-8080-exec-2@5076这类打杂的线程。

  1. public boolean processSocket(SocketWrapperBase<S> socketWrapper,SocketEvent event, boolean dispatch) {
  2. Executor executor = getExecutor();
  3. executor.execute(sc);
  4. return true;
  5. }

给个图的话,大概就是如下的红线流程部分了:

小结

好了,到了课后思考时间了,我们也说了,最终会交给http-nio-8080-exec-2@5076这类线程所在的线程池,那假设这些线程全都在sleep,会发生什么呢?

下一篇,我们继续。

曹工说Tomcat:200个http-nio-8080-exec线程全都被第三方服务拖住了,这可如何是好(上:线程模型解析)的更多相关文章

  1. 曹工说Spring Boot源码(18)-- Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  2. 曹工说Tomcat1:从XML解析说起

    一.前言 第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主13年校招进华为,14年在东莞出差,给东莞移动的通信设备进行版本更新.他们那边的一个小伙子来接我的时候,这么叫我的,刚听到的时候,心里一 ...

  3. 曹工说Spring Boot源码(19)-- Spring 带给我们的工具利器,创建代理不用愁(ProxyFactory)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  4. 曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  5. 曹工说Spring Boot源码(25)-- Spring注解扫描的瑞士军刀,ASM + Java Instrumentation,顺便提提Jar包破解

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  6. 【曹工杂谈】Maven源码调试工程搭建

    Maven源码调试工程搭建 思路 我们前面的文章<[曹工杂谈]Maven和Tomcat能有啥联系呢,都穿打补丁的衣服吗>分析了Maven大体的执行阶段,主要包括三个阶段: 启动类阶段,负责 ...

  7. 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享

    写在前面的话&&About me 网上写spring的文章多如牛毛,为什么还要写呢,因为,很简单,那是人家写的:网上都鼓励你不要造轮子,为什么你还要造呢,因为,那不是你造的. 我不是要 ...

  8. 曹工说Spring Boot源码(20)-- 码网灰灰,疏而不漏,如何记录Spring RedisTemplate每次操作日志

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  9. 曹工说Spring Boot源码(22)-- 你说我Spring Aop依赖AspectJ,我依赖它什么了

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. 手写Promise简易版

    话不多说,直接上代码 通过ES5的模块化封装,向外暴露一个属性 (function(window){ const PENDING = 'pending'; const RESOLVED = 'fulf ...

  2. SCE信号期末复习省流小助手(懒人版)

    XDU-SCE网信院信号期末复习省流小助手(懒人版) 本人根据西安电子科技大学网络与信息安全18年期末考试整理的考点和题型 以下题型代表了信号与系统课程的 精髓 若能掌握以下知识点和题型,80分稳有: ...

  3. C语言复习-字符串与指针

    C语言复习-字符串与指针 例一: [字符串处理 去除C代码中的注释] C/C++代码中有两种注释,/* */和//.编译器编译预处理时会先移除注释.就是把/*和*/之间的部分去掉,把//以及之后的部分 ...

  4. Gradle Wrapper

    Gradle Wrapper 当把本地一个项目放入到远程版本库的时候,如果这个项目是以gradle构建的,那么其他人从远程仓库拉取代码之后如果本地没有安装过gradle会无法编译运行,如果对gradl ...

  5. 安装openssl后yum不能使用的解决办法

    重新编译安装ioenssl后,发现yum命令不能使用,找到如下解决办法 提示错误是 openssl: /usr/lib/x86_64-linux-gnu/libssl.so.1.1: version ...

  6. Python发送get、post请求

    import json import requests #获取北京天气 # #url= "https://wis.qq.com/weather/common?source=xw&we ...

  7. Python编程进阶,Python如何实现多进程?

    进程part1 一.进程介绍 1.获取子父进程 2.进程的基本使用 2.创建带有参数的进程 3.进程之间的数据彼此隔离 4.多个进程之间的异步并发 二.join 1.基本语法 2.使用自定义类的方法创 ...

  8. 上海做假证t

    上海做假证[电/薇:187ヘ1184ヘ0909同号]办各类证件-办毕业证-办离婚证,办学位证书,办硕士毕业证,办理文凭学历,办资格证,办房产证不. 这是一个简单的取最大值程序,可以用于处理 i32 数 ...

  9. jsBridge

    jsBridge https://www.dazhuanlan.com/2019/12/05/5de7eb50739df/ JSBridge的原理 https://juejin.im/post/5ab ...

  10. ajax发送请求的时候url为空或者undefined会发生什么

    $.ajax()里的url为空,ajax请求发送到当前自己的页面. 例如index.html里$.ajax()的url为空就发送到index.html