ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot 的一个 Web 请求会占用一个线程吗?

Spring Boot 的一个 Web 请求会占用一个线程吗? 用 Spring MVC Tomcat 写同步接口时一个请求进入业务代码后通常会占用一个 Tomcat 工作线程直到这次处理结束。线程来自线程池处理完还会接下一个请求并不是每来一个请求就新建一条线程。这个模型有个容易忽略的地方请求在等数据库、Redis 或第三方接口返回时工作线程也被占着。哪怕 CPU 使用率很低服务仍然可能因为没有空闲线程而迟迟无法响应。下面先按平台线程和同步接口来说明再看异步处理与虚拟线程。Spring Boot 也可以使用 WebFlux 等技术栈不能把这一套模型套到所有 Spring Boot 应用上。请求怎样进入业务代码以 Tomcat 的 NIO Connector 为例Acceptor 负责接收连接Poller 监听网络就绪事件工作线程负责后续请求处理。进入 Spring MVC 后常见调用链是Tomcat 工作线程 → Filter → DispatcherServlet → Controller → Service / DAO同步调用下Controller、Service、DAO 通常就在这条工作线程上依次执行调用完再逐层返回。TCP 连接和工作线程也不是一一绑定的一个空闲的 Keep-Alive 连接不需要一直占着工作线程等下一个请求。在使用 Tomcat 默认平台线程池时可以通过下面的配置设置工作线程上限server:tomcat:threads:max:200这里的 200 限制的是请求处理线程数不是 TCP 连接数也不是每秒能处理的请求数。换了自定义 Executor要看那个 Executor 自己的配置启用虚拟线程后这个参数也不再生效。等待 I/O 时线程还在占用中假设接口中有这样一条查询UseruseruserMapper.selectById(id);如果查询耗时 3 秒其中大部分时间都在等 MySQL 返回那么应用的工作线程不会持续消耗 CPU但也不能趁这个空当去处理另一个同步请求。调用没有返回这条线程就还留在当前请求里。把规模放大就容易看出问题了。假设 200 条工作线程中80 条在等数据库50 条在等 Redis40 条在等第三方 HTTP剩下 30 条正在执行其他代码。这只是一个示例但此时确实可能出现 CPU 使用率不高、工作线程却全部忙碌的情况。新请求只能等待调度积压继续扩大后就会出现超时或拒绝。因此CPU 使用率反映不了还有多少工作线程可用。不过低 CPU 也不能单独证明线程池耗尽仍然要看忙碌线程数和线程栈。数据库连接池怎样拖住工作线程Tomcat 线程池和 HikariCP 连接池管理的是两种资源。前者提供执行请求的线程后者提供访问数据库的连接。假设 HikariCP 最多允许 20 个连接而这 20 个连接都被慢查询占着。后续请求进入 DAO 层时可能连 SQL 都还没发出去就先卡在了获取连接这一步。等待连接期间Tomcat 工作线程仍然被这个请求占用。如果请求持续到来等待连接的线程越来越多就可能把 200 条工作线程也占满。最终看到的现象是接口普遍变慢原因却可能从几条慢 SQL 开始查询变慢 → 连接持有时间变长 → 连接池无空闲连接 → 更多工作线程等待连接这个等待有上限。HikariCP 的connectionTimeout控制获取连接最多等多久超时会抛出异常它不负责限制 SQL 的执行时间。排查时可以把线程栈和连接池指标放在一起看Active接近上限、Idle为 0、Pending持续增加说明正在发生连接争用。再看拿到连接的请求在做什么执行慢 SQL、等待数据库锁还是在事务里调用了一个很慢的外部接口。连接被占满只是当前状态原因还在持有连接的那段代码和它依赖的服务里。单纯加大 Tomcat 线程池可能只是让更多线程排队等连接加大数据库连接池也要看数据库能不能承受更多并发。两种容易混淆的异步请求处理不一定始终绑定同一条线程但要区分“派生一个后台任务”和“异步处理这个 HTTP 请求”。用Async派生任务比如创建订单后再异步发送通知PostMapping(/orders)publicResultcreateOrder(){orderService.createOrder();notifyService.sendNotification();// 经 Spring 代理调用的 Async 方法returnResult.success();}在已启用异步支持、通过 Spring 代理调用的前提下sendNotification()会交给配置的 Executor 执行。Tomcat 工作线程继续执行 Controller 的剩余代码并返回响应通知任务可以在响应结束后继续运行。这并没有把当前 HTTP 请求变成 Servlet 异步请求。如果工作线程提交任务后又调用Future.get()或join()等结果它还是会停在那里等。默认代理模式下同一个对象内部直接调用自己的Async方法也不会触发异步执行。若使用ThreadPoolTaskExecutor仍需配置合适的corePoolSize、maxPoolSize、queueCapacity和拒绝策略。把任务挪到另一个线程池后那个线程池同样可能排队或耗尽。用 Servlet 异步处理请求Spring MVC 的异步请求处理支持 Controller 返回Callable、DeferredResult等类型。此时可以先退出 Filter / Servlet 调用链释放当前 Tomcat 工作线程同时让 HTTP 响应保持打开。结果准备好后再调度回容器完成响应。两者的区别在于请求是否还在等待结果前面的通知任务不影响已经返回的响应这里则是响应尚未完成只是等待期间不必一直占着原来的容器线程。异步请求也不意味着业务调用自动变成非阻塞。例如Callable里的阻塞 SQL依旧会占用执行它的线程只是换了执行位置。虚拟线程改变的是等待成本Java 21 通过 JEP 444 将虚拟线程作为正式特性提供。虚拟线程仍然可以按同步方式执行代码在大多数受支持的阻塞 I/O 中等待时可以从承载它的平台线程上卸载让平台线程执行其他虚拟线程。这样就减少了大量请求等待 I/O 时占用平台线程的成本。Spring Boot 从 3.2 开始支持虚拟线程需要运行在 Java 21 或更高版本上spring:threads:virtual:enabled:true在 Spring Boot 自动配置的嵌入式 Tomcat 中启用虚拟线程会改变请求任务的执行方式前面的server.tomcat.threads.max不再作为工作线程上限。自己创建的线程池则不会因为这个开关自动变成虚拟线程执行器。虚拟线程适合并发量大、等待时间占比高的任务。它不会让单条 SQL 跑得更快也不会增加数据库连接数。20 个连接被占满后后续请求仍然要等区别是等待可以更便宜。计算密集型任务的瓶颈仍然在 CPUJava 21 中还存在synchronized等场景下的 pinning不能假设所有阻塞都会释放承载线程。服务变慢时沿着等待位置往下查对使用平台线程的服务可以先在故障期间抓取线程 dumpjstack-lpid间隔几秒抓取几份比只看一次更容易判断哪些调用长期没有推进。重点找 Tomcat 请求线程集中停在哪里再用对应的监控或日志核对线程栈里的位置接下来核对什么HikariCP 获取连接Active、Idle、Pending连接持有时间JDBC 调用或数据库连接的 socket read慢 SQL、数据库锁、数据库和网络状态Redis 或 HTTP 客户端调用下游耗时、超时记录、客户端连接池锁等待谁持有锁持有期间执行了什么Future.get()、join()被等待任务的线程池是否排队任务卡在哪里线程状态也要和调用栈一起读。Java 的RUNNABLE并不保证线程正在消耗 CPUWAITING也不一定异常空闲线程池的工作线程本来就可能在等待任务。Java 线程状态文档描述的是 JVM 层面的状态不能只数几个状态就判断服务是否繁忙。如果启用了虚拟线程要用能观察虚拟线程的 dump例如 Java 21 提供的线程转储命令jcmdpidThread.dump_to_file-formatjson /tmp/threads.json传统jstack不能给出完整的虚拟线程视图。线程栈能指出请求正在等什么但还需要把等待位置和同一时段的流量、连接池、慢查询、下游耗时对上。确认是哪里先变慢、又拖住了哪些资源才能决定该修 SQL、缩短事务、调整超时还是增加容量。原文地址Spring Boot 的一个 Web 请求会占用一个线程吗
返回列表