【023】线程池参数配错致任务堆积?哇哥教王二自定义 Executor|Java 并发篇 2

文章目录
零、引入
“库存同步又卡了!” 王二的哀嚎再次响彻技术部 —— 他用 FixedThreadPool 跑库存任务,结果 1000 个任务堆积在队列里,用户下单后半天看不到库存变化,客服电话被打爆。
杰哥凑过来看了眼代码,乐了:“你给 FixedThreadPool 配了 10 个线程,队列用的是无界队列,任务多了全堵在队列里,能不慢吗? Executors 的现成池是‘快餐’,能救急但不适合所有场景,今天教你‘自己做饭’—— 自定义线程池。”
这篇我们跟着杰哥,搞懂线程池的核心参数,学会自定义 Executor,再搞定定时任务线程池**,点赞 + 关注,让你的并发代码不仅稳,还能 “按需调速”!**

一、新坑:FixedThreadPool 的 “无界队列” 陷阱
王二的代码还是上次的 FixedThreadPool,但这次任务从 1000 涨到了 10000,结果队列里堆了 9990 个任务,用户催单催到客服求饶。
1.1 杰哥扒出 FixedThreadPool 的 “真面目”

“你以为 FixedThreadPool 是‘线程 + 队列’,其实它的队列是无界的(LinkedBlockingQueue),” 杰哥打开源码,“任务超过线程数后全堆队列里,队列能无限长,虽然不会 OOM,但任务执行慢如蜗牛 —— 这就是‘看似安全,实则低效’。”
FixedThreadPool 源码(杰哥划重点)
// Executors.newFixedThreadPool()的源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); // 无界队列
}
“核心线程数 = 最大线程数,队列无限长,意味着永远不会开新线程,任务全排队 —— 你 10 个线程处理 10000 个任务,用户能不投诉吗?”
1.2 解决思路:自定义线程池,配 “有界队列 + 最大线程数”
杰哥说,自定义线程池的核心是ThreadPoolExecutor类 —— 它是 Executor 框架的 “核心实现”,所有 Executors 的现成池,本质都是它的 “包装品”。只要搞懂它的 7 个核心参数,就能配出 “量身定制” 的线程池。
二、 💯线程池核心参数:杰哥用 “公司团队” 比喻秒懂【面试必问】
“线程池就像一个‘任务处理团队’,” 杰哥拿公司做比喻,“7 个参数就是团队的‘管理制度’,配好了效率翻倍,配错了全是坑。”
ThreadPoolExecutor 的 7 个核心参数(公司版解读)
// 自定义线程池的核心构造方法
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(正式工数量)
int maximumPoolSize, // 最大线程数(正式工+临时工总数)
long keepAliveTime, // 临时工空闲时间(超过就辞退)
TimeUnit unit, // 空闲时间单位(比如秒)
BlockingQueue<Runnable> workQueue, // 任务队列(等待区)
ThreadFactory threadFactory, // 线程工厂(招人用的)
RejectedExecutionHandler handler // 拒绝策略(任务太多装不下时怎么办)
)
杰哥的 “团队管理” 比喻
- 核心线程数(corePoolSize):正式工,不管忙不忙都不辞退;比如设 5,就有 5 个线程一直存活;
- 最大线程数(maximumPoolSize):团队总人数,正式工 + 临时工;比如设 10,就最多能有 10 个线程同时干活;
- 空闲时间(keepAliveTime):临时工没事干多久后辞退;比如设 60 秒,临时工空闲 60 秒就销毁;
- 任务队列(workQueue):等待区,正式工忙时,任务先放这里;比如用有界队列,最多放 100 个任务;
- 拒绝策略(handler):等待区满了,临时工也招满了,新任务怎么办?(比如给用户返回 “系统繁忙”)。
“比如你配核心 5、最大 10、队列 100,” 杰哥总结,“任务过来先让 5 个正式工干,干不过来放队列;队列满了招临时工(最多招到 10 个);临时工也满了,就执行拒绝策略。”
三、王二的自定义线程池:解决库存任务堆积
杰哥帮王二分析库存同步场景:“任务执行时间 100ms,高峰期 1000 个任务 / 秒,核心线程设 5,最大 10,队列设 200,这样既不会堆任务,也不会开太多线程。”
自定义线程池代码(王二直接抄上线)
import java.util.concurrent.*;
public class CustomThreadPoolDemo {
static final int TASK_NUM = 10000; // 高峰期10000个任务
static CountDownLatch latch = new CountDownLatch(TASK_NUM);
public static void main(String[] args) {
// 1. 自定义线程池的7个参数
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5, // 核心线程数(正式工5人)
10, // 最大线程数(正式工+临时工共10人)
60, // 临时工空闲60秒辞退
TimeUnit.SECONDS, // 时间单位:秒
new ArrayBlockingQueue<>(200), // 有界队列,最多放200个任务
Executors.defaultThreadFactory(), // 默认线程工厂
// 拒绝策略:任务太多时,抛异常并记录日志(自己实现)
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println("任务被拒绝,当前队列大小:" + executor.getQueue().size());
// 实际场景:可以把任务存到Redis,后续重试
}
}
);
for (int i = 0; i < TASK_NUM; i++) {
int goodsId = i;
threadPool.submit(() -> {
try {
System.out.println("同步商品" + goodsId + "库存,线程:" + Thread.currentThread().getName());
Thread.sleep(100); // 模拟数据库耗时
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
try {
latch.await();
System.out.println("所有库存同步完成,总耗时:" + (System.currentTimeMillis() - start) + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
运行效果:任务不堆积,响应快一倍
王二跑起来发现,任务最多排队 200 个,超过后会触发拒绝策略(存 Redis 重试),10 个线程同时干活,总耗时比之前的 FixedThreadPool 快了 5 倍,用户再也没投诉过。
四、拒绝策略:4 种现成方案,不用自己写

“拒绝策略不用每次都自己实现,Java 给你备了 4 种现成的,” 杰哥说,就像外卖员送不下餐时的处理方式,按需选就行。
王二的小改造:用 CallerRunsPolicy 处理日志任务
比如处理用户登录日志,任务失败也不影响核心业务,用 CallerRunsPolicy 最合适:
// 拒绝策略换成CallerRunsPolicy
new ThreadPoolExecutor.CallerRunsPolicy()
五、定时任务线程池:ScheduledExecutorService
“除了普通任务,还有定时任务要处理,比如每天凌晨 2 点同步库存,” 杰哥说,“用 ScheduledExecutorService,比 Timer 靠谱 10 倍 ——Timer 是‘单身狗’,一个任务抛异常全崩;它是‘团队作战’,一个线程挂了不影响其他的。”
定时任务代码:每天凌晨 2 点同步库存
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledExecutorDemo {
public static void main(String[] args) {
// 创建定时任务线程池,核心线程数=1
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(1);
// 任务:每天凌晨2点同步库存
Runnable stockTask = () -> {
System.out.println("开始同步全量库存...");
// 模拟库存同步耗时
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("库存同步完成");
};
// 计算当前时间到凌晨2点的延迟
long delay = calculateDelayTo2AM();
// scheduleAtFixedRate:固定频率执行
// 参数:任务、初始延迟、周期、时间单位
scheduledPool.scheduleAtFixedRate(stockTask, delay, 24, TimeUnit.HOURS);
}
// 计算当前时间到凌晨2点的毫秒数
private static long calculateDelayTo2AM() {
long now = System.currentTimeMillis();
long next2AM = now - (now % (24 * 60 * 60 * 1000)) + (2 * 60 * 60 * 1000);
if (next2AM <= now) {
next2AM += 24 * 60 * 60 * 1000;
}
return (next2AM - now) / 1000;
}
}
❌❌ 杰哥的提醒:别用 Timer!
“以前用 Timer 做定时任务,一个任务抛异常,整个 Timer 就崩了,所有定时任务全停了,” 杰哥说,“ScheduledExecutorService 每个任务一个线程,互相不影响,线上环境必须用它。”
六、线程池配置的 “黄金法则”(王二贴在显示器上)

王二把杰哥的配置经验记成 “三大法则”,再也没配错过线程池:
- 核心线程数:看任务类型
- CPU 密集型(比如计算):核心线程数 = CPU 核心数 + 1(充分利用 CPU);
- IO 密集型(比如查数据库、调用接口):核心线程数 = CPU 核心数 * 2(因为线程大部分时间在等 IO);
- 队列用有界的:别用 LinkedBlockingQueue(无界),用 ArrayBlockingQueue 并设大小(比如 200),避免任务堆积;
- 拒绝策略要兜底:核心业务用 AbortPolicy(抛异常让上层处理),非核心用 CallerRunsPolicy 或存 Redis 重试;
➡️➡️ 最后说句实在的
Executor 框架不是 “复杂技术”,而是 Java 给你的 “并发工具箱”——Executors 的现成池是 “快餐”,适合快速开发;
ThreadPoolExecutor 是 “家常菜”,适合量身定制。
记住:线程池的核心是 “平衡线程数和任务数”,既不能让线程闲得慌,也不能让任务堆成山。




更多推荐


所有评论(0)