还在 new Thread 吗?SpringBoot 2.7.18 线程池暗坑与急救指南

B站影视 日本电影 2025-10-06 08:49 1

摘要:本文用 SpringBoot 2.7.18 + JDK 8 演示“一条任务”在 new Thread 与自定义线程池两条路线下的完整生命周期,给出可复制的急救代码与动态调参方案。

痛点共鸣解决方向

生产环境一高峰就 OOM,dump 一看全是 new Thread 创建的“野生”线程;想改线程池,又怕踩阿里手册里“禁止 Executors 工厂”的雷。

90% 的 SpringBoot 业务代码都写过 new Thread( -> {}).start,本地没毛病,上线就爆。

本文用 SpringBoot 2.7.18 + JDK 8 演示“一条任务”在 new Thread 与自定义线程池两条路线下的完整生命周期,给出可复制的急救代码与动态调参方案。

new Thread:每任务一条内核线程,由 JVM 直接映射到 OS LWP,创建/销毁成本“毫秒级”。•线程池:维护一组可复用的 Worker,任务以 Runnable/Callable 形式进入阻塞队列,Worker 循环取任务执行。

•1.0 时代:Thread/Runnable

•5.0 时代:ExecutorService + ThreadPoolExecutor

•8.0 时代:CompletableFuture 与并行流

•SpringBoot 2.7.x:默认未托管线程池,需手动声明 Bean;Actuator 提供“线程池度量”端点。

图 1:new Thread 与线程池两条路径对比

图 1 解释
红色路线:每次“新建-销毁”消耗内核资源;绿色路线:线程被复用,开销仅一次创建。

new Thread 暗坑线程池暗坑1. 无上限创建 → OOM
2. 无法统一异常捕获
3. 无法优雅关闭1. 队列无界 → OOM
2. 拒绝策略选错 → 任务静默丢失
3. 核心参数写死 → 高峰仍被打爆

•Executors.newCachedThreadPool:最大线程数 ≈ Integer.MAX_VALUE,阿里手册直接拉黑。

•Executors.newFixedThreadPool:队列默认 LinkedBlockingQueue 无界,任务无限堆积。

图 2:任务在两种模式下的资源占用时间线

图 2 解释
红色背景:每次伴随 1 MB 内核栈分配+释放;绿色背景:栈只申请一次,后续任务“零”额外内存。

图 3:SpringBoot 项目线程池托管架构

图 3 解释
线程池以 @Bean 形式被 Spring 托管,BizService 仅依赖 ExecutorService 接口;Actuator 将队列大小、活跃线程数打成 Metrics,供 Prometheus 拉取。

组件职责ThreadPoolExecutor真正执行任务ResizableBlockingQueue自定义可动态缩容/扩容队列ThreadPoolMetrics将 coreSize、queueSize 注册到 MicrometerNacosConfigListener监听配置中心变更,实时 setCorePoolSize

以下代码均基于 SpringBoot 2.7.18 + JDK 8,可直接复制运行。

4.0.0「包名称,请自行替换」thread-pool-demo1.0.0

org.springframework.bootspring-boot-starter-parent2.7.18

org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-actuatorio.micrometermicrometer-registry-prometheus

package 「包名称,请自行替换」.config;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.TimeUnit;/*** 支持运行时动态 setCapacity 的阻塞队列* 注意:缩容时仅阻止新元素加入,不会主动踢出已有任务*/public class ResizableBlockingQueue extends LinkedBlockingQueue {private volatile int capacity;public ResizableBlockingQueue(int capacity){super(capacity);this.capacity = capacity;}public void setCapacity(int newCapacity){this.capacity = newCapacity;}@Overridepublic boolean offer(E e){/* 缩容场景:当队列已有元素 >= newCapacity 时,offer 返回 false,线程池会走拒绝策略;扩容场景直接放行。 */if(size >= capacity){return false;}return super.offer(e);}}package 「包名称,请自行替换」.config;import org.springframework.boot.actuate.metrics.MetricsEndpoint;import org.springframework.context.annotation.bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;@Configurationpublic class ThreadPoolConfig {/*** 手动创建线程池,拒绝策略:CallerRunsPolicy* 防止任务被静默丢弃*/@Bean("bizExecutor")public ThreadPoolTaskExecutor bizExecutor{ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor;executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setKeepAliveSeconds(60);/* 关键:替换默认队列为可调整容量队列 */ResizableBlockingQueue queue = new ResizableBlockingQueue(200);executor.setQueueCapacity(200); // 仅做初始值executor.setThreadNamePrefix("biz-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy);executor.setWaitForTasksToCompleteOnShutdown(true);executor.setAwaitTerminationSeconds(30);executor.initialize;/* 将队列注入到 executor 内部(反射方式,略)*/executor.setQueue(queue);return executor;}}package 「包名称,请自行替换」.service;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.stereotype.Service;import javax.annotation.Resource;import java.util.concurrent.CompletableFuture;import java.util.concurrent.Executor;@Servicepublic class BizService {@Resource@Qualifier("bizExecutor")private Executor executor;/*** 模拟下单异步通知* 安全提示:此处为演示,生产请做好幂等/重试*/public CompletableFuture notifyDownStream(Long orderId){return CompletableFuture.supplyAsync( -> {try {// 模拟网络 IOThread.sleep(500);} catch (InterruptedException e) {Thread.currentThread.interrupt;}return "ok-" + orderId;}, executor);}}package 「包名称,请自行替换」.controller;import 「包名称,请自行替换」.config.ResizableBlockingQueue;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import java.util.HashMap;import java.util.Map;@RestController@RequestMapping("/pool")public class PoolController {@Resource@Qualifier("bizExecutor")private ThreadPoolTaskExecutor executor;@GetMapping("/status")public Map status{Map map = new HashMap;map.put("coreSize", executor.getCorePoolSize);map.put("maxSize", executor.getMaxPoolSize);map.put("activeCount", executor.getActiveCount);map.put("queueSize", executor.getThreadPoolExecutor.getQueue.size);return map;}@PostMapping("/resize")public String resize(@RequestParam int core,@RequestParam int max,@RequestParam int queue) {executor.setCorePoolSize(core);executor.setMaxPoolSize(max);/* 调整队列容量 */ResizableBlockingQueue queueImpl =(ResizableBlockingQueue) executor.getThreadPoolExecutor.getQueue;queueImpl.setCapacity(queue);return "ok";}}

1.拒绝策略务必选 CallerRunsPolicy 或自定义带报警的 RejectedExecutionHandler,避免任务静默丢弃。

2.队列容量 = 可接受最大延迟任务数,建议 (峰值 QPS × 最大容忍秒数) 再留 20% 缓冲

3.核心线程数参考 CPU 核数 × (1 + 平均等待时间/平均计算时间),I/O 密集可上调。

4.使用 actuator/metrics 实时采集 thread.pool.active、thread.pool.queue.size,联动告警。

package 「包名称,请自行替换」;import 「包名称,请自行替换」.service.BizService;import org.junit.jupiter.api.Test;import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;import java.util.concurrent.CompletableFuture;import java.util.stream.IntStream;@SpringBootTestclass PoolCompareApplicationTests {@Resourceprivate BizService bizService;@Testvoid flood {long start = System.currentTimeMillis;/* 瞬间提交 1000 任务 */CompletableFuture.allOf(IntStream.range(0, 1000).mapToObj(i -> bizService.notifyDownStream((long) i)).toArray(CompletableFuture::new)).join;System.out.println("cost = " + (System.currentTimeMillis - start) + " ms");}}方向做法潜在问题解决策略动态线程结合 Nacos 配置监听,实时调整 core/max/queue缩容过快导致拒绝滑动窗口检测,连续 3 个周期低于 30% 才缩容异步编排CompletableFuture 链式调用守护线程导致 JVM 提前退出自定义 ThreadFactory 将 daemon 设为 false任务追溯给 Runnable 包装 MDC traceId线程复用导致日志串号在 beforeExecute/afterExecute 清理 MDC多池隔离查询/写入/上报分别独立线程池线程数翻倍共用 ForkJoinPool.commonPool 做计算,I/O 任务再丢给独立池

核心回顾:new Thread 适合“一次性、低并发”场景;生产环境务必使用自定义 ThreadPoolExecutor,并暴露动态调参与监控。

JDK:8u351+(验证命令 java -version)

Maven:3.8+

OS:Windows / macOS / Linux 均可

thread-pool-demo├── pom.xml├── src│ └── main│ ├── java│ │ └── 「包名称,请自行替换」│ │ ├── Application.java # @SpringBootApplication│ │ ├── config│ │ │ ├── ResizableBlockingQueue.java│ │ │ └── ThreadPoolConfig.java│ │ ├── controller│ │ │ └── PoolController.java│ │ └── service│ │ └── BizService.java│ └── resources│ └── application.yml└── src/test/java/.../PoolCompareApplicationTests.java# application.ymlserver:port: 8080management:endpoints:web:exposure:include: metrics,prometheusmetrics:tags:application: ${spring.application.name}spring:application:name: thread-pool-demo

1.本地编译

mvn clean package -DskipTests

2.启动应用

java -jar target/thread-pool-demo-1.0.0.jar

成功标志:控制台出现Started Application

4.验证监控
浏览器访问http://localhost:8080/actuator/metrics能看到executor.active、executor.queue.size。

5.压测

本文所有案例代码、配置仅供参考,如需使用请严格做好相关测试及评估,对于因参照本文内容进行操作而导致的任何直接或间接损失,作者概不负责。本文旨在通过生动易懂的方式分享实用技术知识,欢迎读者就技术观点进行交流与指正。

来源:冷不叮的小知识

相关推荐