深入拆解TomcatJetty——Tomcat如何实现IO多路复用

深入拆解Tomcat&Jetty

专栏地址: 极客时间-深入拆解Tomcat & Jetty

IO 多路复用

当用户线程发起 I/O 操作后,网络数据读取操作会经历两个步骤:

  • 用户线程等待内核将数据从网卡拷贝到内核空间。
  • 内核将数据从内核空间拷贝到用户空间。

IO 多路复用 是 Linux 五种 IO 模型中的一种,逻辑如下图:

image-20241025140303833

这时用户线程读取数据分为了两步:

  • 间断的发起 select 调用询问内核数据是否已经准备好
  • 在数据就绪后发起 read 系统调用,注意在数据从内核空间拷贝到用户空间时,线程依然是阻塞的

多路复用体现在一次 select 调用可以查询多个 数据通道(channel) 上的数据是否已经准备好

Tomcat 如何实现多路复用模型

对于 Java 的多路复用器的使用,无非是两步:

  • 创建一个 Selector,在它身上注册各种感兴趣的事件,然后调用 select 方法,等待感兴趣的事情发生。
  • 感兴趣的事情发生了,比如可以读了,这时便创建一个新的线程从 Channel 中读数据。

Tomcat 的 NioEndpoint 组件虽然实现比较复杂,但基本原理就是上面两步。它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor 和 Executor 共 5 个组件,它们的工作过程如下图所示:

img

  • LimitLatch:连接控制器,它负责控制最大连接数,NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝(这里的拒绝指的是应用层意义上的拒绝,操作系统依然会接收 socket 连接,直到等待队列满)。
  • Acceptor 跑在一个单独的线程里,它在一个死循环里调用 accept 方法来接收新连接,一旦有新的连接请求到来,accept 方法返回一个 Channel 对象,接着把 Channel 对象交给 Poller 去处理。
  • Poller 的本质是一个 Selector,也跑在单独线程里。Poller 在内部维护一个 Channel 数组,它在一个死循环里不断检测 Channel 的数据就绪状态,一旦有 Channel 可读,就生成一个 SocketProcessor 任务对象扔给 Executor 去处理。
  • Executor 就是线程池,负责运行 SocketProcessor 任务类,SocketProcessor 的 run 方法会调用 Http11Processor 来读取和解析请求数据。Http11Processor 是应用层协议的封装,它会调用容器获得响应,再把响应通过 Channel 写出。

LimitLatch(org.apache.tomcat.util.threads.LimitLatch)

部分核心代码如下:

public class LimitLatch {
    private class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected int tryAcquireShared() {
            long newCount = count.incrementAndGet();
            if (newCount > limit) {
                count.decrementAndGet();
                return -1;
            } else {
                return 1;
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            count.decrementAndGet();
            return true;
        }
    }

    private final Sync sync;
    private final AtomicLong count;
    private volatile long limit;
    
    //线程调用这个方法来获得接收新连接的许可,线程可能被阻塞
    public void countUpOrAwait() throws InterruptedException {
      sync.acquireSharedInterruptibly(1);
    }

    //调用这个方法来释放一个连接许可,那么前面阻塞的线程可能被唤醒
    public long countDown() {
      sync.releaseShared(0);
      long result = getCount();
      return result;
   }
    
}
  • 内部类 Sync 继承了 AQS,AQS 是 Java 并发包中的一个核心类,它在内部维护一个状态和一个线程队列,可以用来控制线程什么时候挂起,什么时候唤醒。
  • 用户线程通过调用 LimitLatch 的 countUpOrAwait 方法来拿到锁,如果暂时无法获取,这个线程会被阻塞到 AQS 的队列中。而这个方法实际会调用拓展类所重写的 tryAcquireShared 方法,它的实现逻辑是如果当前连接数 count 小于 limit,线程能获取锁,返回 1,否则返回 -1。
  • 如果用户线程被阻塞到了 AQS 的队列,同样是由 Sync 内部类决定唤醒,Sync 重写了 AQS 的 tryReleaseShared() 方法,其实就是当一个连接请求处理完了,这时又可以接收一个新连接了,这样前面阻塞的线程将会被唤醒。

Acceptor(org.apache.tomcat.util.net.Acceptor)

Acceptor 实现了 Runnable 接口,因此可以跑在单独线程里。

一个端口号只能对应一个 ServerSocketChannel,因此这个 ServerSocketChannel 是在多个 Acceptor 线程之间共享的,它是 Endpoint 的属性,由 Endpoint 完成初始化和端口绑定。初始化过程如下:

// org.apache.tomcat.util.net.NioEndpoint.initServerSocket()
serverSock = ServerSocketChannel.open();
// getAcceptCount() Acceptor负责从ACCEPT队列中取出连接,当Acceptor处理不过来时,连接就堆积在ACCEPT队列中,默认100
serverSock.socket().bind(addr, getAcceptCount());
serverSock.configureBlocking(true);

ServerSocketChannel 通过 accept() 接受新的连接,accept() 方法返回获得 SocketChannel 对象,然后将 SocketChannel 对象封装在一个 PollerEvent 对象中,并将 PollerEvent 对象压入 Poller 的 Queue 里,这是个典型的“生产者 - 消费者”模式,Acceptor 与 Poller 线程之间通过 Queue 通信。

NioEndpoint.start->
    startInternal()->
    startAcceptorThread() {new Thread(acceptor, threadName).start()} -> 
    Acceptor.run(){ socket = endpoint.serverSocketAccept(); endpoint.setSocketOptions(socket); } ->
    NioEndpoint.setSocketOptions(socke){ NioSocketWrapper socketWrapper = new Nio2SocketWrapper(channel, this); poller.register(); } ->
    NioEndpoint.register(socketWrapper){ addEvent(new PollerEvent(socketWrapper, OP_REGISTER)); } ->
    NioEndpoint.events.offer(event){ Poller.events.offer(event); }

Poller(org.apache.tomcat.util.net.NioEndpoint.Poller)

Poller 本质是一个 Selector,它内部维护一个 Queue,这个 Queue 定义如下:

private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();

SynchronizedQueue 的方法比如 offer、poll、size 和 clear 方法,都使用了 synchronized 关键字进行修饰,用来保证同一时刻只有一个 Acceptor 线程对 Queue 进行读写。同时有多个 Poller 线程在运行(Tomcat9只有一个线程在运行,NioEndpoint#startInternal()),每个 Poller 线程都有自己的 Queue。每个 Poller 线程可能同时被多个 Acceptor 线程调用来注册 PollerEvent。同样 Poller 的个数可以通过 pollers 参数配置。

  • Poller 不断的通过内部的 Selector 对象向内核查询 Channel 的状态,一旦可读就生成任务类 SocketProcessor 交给 Executor 去处理。
  • Poller 的另一个重要任务是循环遍历检查自己所管理的 SocketChannel 是否已经超时,如果有超时就关闭这个 SocketChannel。

SocketProcessor(org.apache.tomcat.util.net.NioEndpoint.SocketProcessor)

Poller 会创建 SocketProcessor 任务类交给线程池处理,而 SocketProcessor 实现了 Runnable 接口,(这里是 SocketProcessorBase 实现了 Runnable 接口,在 run 方法里调用了抽象方法 doRun,SocketProcessor 继承了它并重写了 doRun 方法),用来定义 Executor 中线程所执行的任务,主要就是调用 Http11Processor 组件来处理请求。Http11Processor 读取 Channel 的数据来生成 ServletRequest 对象(Http11Processor#service())。

这里请注意:Http11Processor 并不是直接读取 Channel 的。这是因为 Tomcat 支持同步非阻塞 I/O 模型和异步 I/O 模型,在 Java API 中,相应的 Channel 类也是不一样的,比如有 AsynchronousSocketChannel 和 SocketChannel,为了对 Http11Processor 屏蔽这些差异,Tomcat 设计了一个包装类叫作 SocketWrapper,Http11Processor 只调用 SocketWrapper 的方法去读写数据。

Executor

Executor 是 Tomcat 定制版的线程池,它负责创建真正干活的工作线程,干什么活呢?就是执行 SocketProcessor 的 run 方法,也就是解析请求并通过容器来处理请求,最终会调用到 Servlet。

如何实现高并发

高并发就是能快速地处理大量的请求,需要合理设计线程模型让 CPU 忙起来,尽量不要让线程阻塞,因为一阻塞,CPU 就闲下来了。另外就是有多少任务,就用相应规模的线程数去处理。我们注意到 NioEndpoint 要完成三件事情:接收连接、检测 I/O 事件以及处理请求,那么最核心的就是把这三件事情分开,用不同规模的线程数去处理,比如用专门的线程组去跑 Acceptor,并且 Acceptor 的个数可以配置;用专门的线程组去跑 Poller,Poller 的个数也可以配置;最后具体任务的执行也由专门的线程池来处理,也可以配置线程池的大小。

这其中比较核心的就是把检测IO事件这一操作由少量selector集中处理,避免大量线程占用cpu时间在轮询IO事件上

Java中自身的NIO到底是同步非阻塞,还是IO多路复用

NIO API 可以不用 Selector,就是同步非阻塞。使用了 Selector 就是 IO 多路复用

Tomcat 该组件虽然是叫 NioEndpoint,但使用了 Selector,所以其实是 IO 多路复用

如何理解 IO 操作模型中的同步异步,阻塞非阻塞

同步异步:

  • 同步可以理解为线程在请求 IO 数据后是直接返回,还是阻塞等待数据从网卡(或者其他地方)拷贝到内存空间再到用户空间
  • 异步可以理解为线程在请求 IO 数据后直接返回,但是在请求时注册了一个回调函数,内核将数据准备好后通过回调函数通知

阻塞和非阻塞主要是看发起I/O操作时,内核空间没有数据可读时,线程是否会阻塞等待,直到有数据到来

  • 阻塞:调用 read() 时,如果内核空间中没有数据可读,线程就让出 cpu 阻塞等待,直到内核把数据拷贝到用户空间,唤醒线程,read()调用返回
  • 非阻塞:调用 read() 时,如果内核空间没有可读数据,线程立刻返回,直到再次调用read(),内核空间有数据可读时,阻塞等待内核把数据拷贝到 read() 函数指定的buff中,唤醒线程,read()调用返回

关于 IO 的几篇文章推荐:

【1】https://time.geekbang.org/column/article/100307 Tomcat如何实现IO多路复用

【2】https://time.geekbang.org/column/article/103959 内核如何阻塞与唤醒进程

【3】https://mp.weixin.qq.com/s/LYbJxorhsyoWWtP6OR6-eQ 一顿饭的事儿,搞懂Linux5种IO模型

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

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

相关文章

面试域——技术面试准备

摘要 来到技术面试这环节有两种情况&#xff0c;其一&#xff1a;这场技术面试可能就是一个面试官KPI面试&#xff08;就是面试工作量&#xff0c;这个面试你是不可能过。&#xff09;如今的就业环境下&#xff0c;人力资源部门也是有考核指标。如果遇到这样的面试你就放平心态…

RabbitMq-队列交换机绑定关系优化为枚举注册

&#x1f4da;目录 &#x1f4da;简介:&#x1f680;比较&#x1f4a8;通常注册&#x1f308;优化后注册 ✍️代码&#x1f4ab;自动注册的关键代码 &#x1f4da;简介: 该项目介绍&#xff0c;rabbitMq消息中间件&#xff0c;对队列的注册&#xff0c;交换机的注册&#xff0c…

SQL进阶技巧:如何求组内排除当前行的移动平均值?

目录 0 需求描述 2 数据准备 3 问题分析 4 小结 0 需求描述 -- 按照 日期,省份,等级 分组 求分数的平均值;但是需要剔除当前行的数据 2 数据准备 create table avgtest as (select 2024-10-24 as cdate, 广东 as province,深圳 as city, 2 as level, 200 as scoreunio…

雷池社区版OPEN API使用教程

OPEN API使用教程 新版本接口支持API Token鉴权 接口文档官方没有提供&#xff0c;有需要可以自行爬取&#xff0c;爬了几个&#xff0c;其实也很方便 使用条件 需要使用默认的 admin 用户登录才可见此功能版本需要 > 6.6.0 使用方法 1.在系统管理创建API TOKEN 2.发…

基于SSM+小程序的汽车保养登录管理系统(汽车1)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于SSM小程序的汽车保养登录管理系统实现了管理员及用户。 1、管理员实现了管理门店和其对应的员工&#xff0c;管理保养信息和汽车配件信息&#xff0c;管理各种状态的订单。 2、用户…

offset Explorer连接云服务上的kafka连接不上

以上配置后报连接错误时&#xff0c;可能是因为kafka的server.properties配置文件没配置好&#xff1a; 加上面两条配置&#xff0c;再次测试连接&#xff0c;成功 listeners和advertised.listeners

若依部署上线遇到的问题

一、若依部署上线的用户头像模块不能回显&#xff1a; 首先是后端修改部署上线后若依存储图片的本地地址 其次将上线前端配置文件中的图片相关配置给删除 二、若依部署上线后验证码不显示问题 在确保前后端请求打通后还有这个问题就是磁盘缓存问题 三、若依部署上线遇到404页…

雷池社区版中升级雷池遇到问题

关于升级后兼容问题 版本差距过大会可能会发生升级后数据不兼容导致服务器无法起来 跨多个版本&#xff08;超过1个大版本号&#xff09;升级做好数据备份&#xff0c;遇到升级失败可尝试重新安装解决 升级提示目录不对 在错误的目录下执行&#xff08;比如 safeline 的子目…

Navicat导入Excel数据时数据被截断问题分析与解决方案

目录 前言1. 问题分析1.1 默认字段类型的影响1.2 MySQL诊断机制的限制 2. 解决方案2.1 修改字段长度2.2 修改Excel数据以影响推断2.3 检查导入工具的设置 3. 其他注意事项3.1 注册表的修改3.2 增加自增ID 4. 结语 前言 在数据库的日常操作中&#xff0c;将Excel数据导入MySQL是…

Android Junit 单元测试 | 依赖配置和编译报错解决

问题 为什么在依赖中添加了testImplement在build APK的时候还是会报错&#xff1f;是因为没有识别到test文件夹是test源代码路径吗&#xff1f; 最常见的配置有: implementation - 所有源代码集(包括test源代码集)中都有该依赖库.testImplementation - 依赖关系仅在test源代码…

前端代码分享--爱心

给对象写的&#xff0c;顺便源码给大家分享一下 就是简单的htmlcssjs&#xff0c;不复杂 xin1.html <!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <title>写你自己的</title> <lin…

buildroot制作自己的软件包(可以理解为应用程序)

以helloworld为例记录使用步骤 一&#xff1a;书写自己的源程序以及Makefile helloworld.c #include <stdio.h>int main(int argc, char **argv) {printf("hello world\r\n");return 0; }Makefile all: helloworldhelloworld: helloworld.o$(CC) -o hellow…

关于嵌入式学习的一些短浅经验

一、写在前面 感谢在 10.23&#xff0c;各位大佬对我进行的模拟面试&#xff0c;我也发现了我对知识的不熟练的部分&#xff0c;比如 IPC 方法和线程同步方法的知识。模拟面试第四期-已经拿到大厂 OFFER 的研究生大佬-LINUX 卷到飞起_哔哩哔哩_bilibili 然后&#xff0c;沈阳…

OpenRTP 传输增加OpenRTPServer

开源地址 最近增加了OpenRTPServer&#xff0c; 已经修改完成一版放在了目录下&#xff0c;window和linux下编译都成功了&#xff0c;不过由于修改代码CMakefile 需要修改&#xff0c;先放放 OpenRTP开源地址 vlc得纠错传输方式 我发现我代码写错以后&#xff0c;vlc 依然能…

重要:民族共同体精品课格式说明

铸牢中华民族共同体意识精品课以微课形式呈现&#xff0c;包括微课 视频、教学设计讲义、课件等。 微课视频 微课视频应采用“教师讲解多媒体大屏”的形式&#xff0c;适当呈现授课教师画面&#xff0c;增强教学的交互性和画面的可视性。单个微课视频时长&#xff1a;高校专题…

【已解决】cannot import name ‘Literal‘ from ‘typing‘

问题描述 在用vscode进行debug的时候&#xff0c;报错cannot import name Literal from typing 解决方法 方法一&#xff1a;升级Python版本到3.8以上 我的python版本是3.7&#xff0c;但由于环境都配好了&#xff0c;升级太麻烦&#xff0c;没采用该方法 方法二&#xff1…

C++和Java该如何进行选择?

曾经的自己与许多C程序员都有着一样的盲目自信&#xff1a;认为掌握了C&#xff0c;在去学习Java上手会容易很多。 到底是谁给了你这种勇气和自信&#xff1f; 很多人经常会说&#xff0c;Java这种通过虚拟机运行的语言&#xff0c;虚拟机本身就是C开发的&#xff0c;根本就没…

Java 多线程(九)—— JUC 常见组件 与 线程安全的集合类

Callable 与 FutureTask Callable 接口和 Runnable 接口是并列关系&#xff0c;都是用来给线程提供任务的&#xff0c;只不过 Callable 接口的任务可以带有返回值。 但是 Callable 接口创建的任务不能直接传入 Thread 里面&#xff0c;这也是为了 解耦合&#xff0c;我们可以使…

pdf合并,这4款好用软件分分钟解决问题!

PDF作为一种跨平台、不易被篡改的文档格式&#xff0c;广泛应用于工作、学习和日常生活中。然而&#xff0c;当面对多个PDF文件需要合并成一个时&#xff0c;繁琐的手动操作往往让人头疼不已。别担心&#xff0c;今天就给大家安利4款超实用的PDF合并软件&#xff0c;它们不仅操…

c++二级指针

如果要通过函数改变一个指针的值&#xff0c;要往函数中传入指针的指针 如果要通过函数改变一个变量的值&#xff0c;那就要往函数中传入这个变量的地址 改变a的值和b的值 #include <iostream>using namespace std;void swap(int* a, int* b) {int temp *a;*a *b;*b …