面试必问的线程池,原来藏着这些致命陷阱!

01 引言

Java高并发的项目,少不了多线程的使用,而多线程的管理自然要使用到线程池。池化的思想是很多架构一定会考虑的要素,可以很好的控制资源的资源的使用,但是使用不好,造成的问题也不容小觑。

多线程的使用需要考虑的因素很多,这也成了面试八股文必问为技术点。

02 线程池的创建

你是否还在这样的创建线程池?

Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
Executors.newFixedThreadPool(5);
Executors.newScheduledThreadPool(5);

这样封装好的线程池的创建方法,可以帮我们快速的创建线程池。在使用线程时,优先使用线程池去创建,阿里巴巴的技术规约强制使用线程池创建线程。

image-20250620155436716

但是线程池里面的核心参数,我们是不清楚的,这也是同样是阿里巴巴的技术规约不允许使用Executors去创建的原因。

使用java.util.concurrent.ThreadPoolExecutor创建线程池,就需要了解其构造方法。

2.1 构造参数解析

corePoolSize:核心线程数。当任务来临时,优先使用核心线程。空闲时也不会销毁。

maximumPoolSize:最大的线程数。允许创建的最大线程数,包含核心线程。最大线程数必须大于等于核心线程。

keepAliveTime:非核心线程的空闲的存活时间,线程超过时间后被销毁。

TimeUnit:存活时间的单位

workQueue:存储需要处理的任务,这是使用的是阻塞队列

threadFactory:线程工厂,定制线程创建(命名、优先级等)

handler:拒绝策略,处理队列满时的任务(如丢弃、抛异常)

创建示例

ExecutorService pool = new ThreadPoolExecutor(
    5,                      			 // 核心线程数
    10,                     			 // 最大线程数
    60, TimeUnit.SECONDS,                // 空闲线程超时时间
    new LinkedBlockingQueue<>(100),      // 任务队列
    Executors.defaultThreadFactory(),    // 默认工厂
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略-直接抛异常
);

2.2 任务调度流程

主要流程就是优先使用核心线程,当核心线程用完时,会将任务存入队列。当队列满后,才会触发创建新线程。而当创建的线程数达到最大线程数时,就会触发拒绝策略。

源码

2.3 线程池的状态

  • RUNNING: 正常接收新任务
  • SHUTDOWN: 不再接收新任务,继续处理队列任务
  • STOP: 中断所有任务,不再处理队列
  • TIDYING: 所有任务终止,线程数为0
  • TERMINATED: 终止状态

03 线程池使用

ExecutorService pool = new ThreadPoolExecutor(
        1,     // 核心线程数              
        2,     // 总线程数
        60, TimeUnit.SECONDS, // 空闲时间 60s
        new LinkedBlockingQueue<>(5),  // 最多存储5个任务
        Executors.defaultThreadFactory(),  // 默认工厂
        new ThreadPoolExecutor.AbortPolicy()  // 默认拒绝策略
);

for (int i = 0; i < 10; i++) {
    pool.execute(() -> System.out.println(DateUtil.now() + " 任务执行了"));
}

pool.shutdown();  

实例里面模拟了10个任务,最后等待所有的任务执行完,关闭线程池。

但是为什么报错了呢?这是因为出发了线程池的拒绝策略。

总线程数是2,队列是5,也就是说线程池最大的并发量是7个任务。当第8个任务来的时候,就是触发线程池的拒绝策略。

3.1 线程池的关闭

线程池是否需要关闭呢?

只需要看线程池是否需要复用。如果线程池定义在方法体内,每一次使用都需要创建线程池,则再使用后需要关闭线程池。因为线程池的核心线程是不会自动关闭的,随着方法调用的次数增多,存活的核心线程数就会增多,最终会引起内存溢出。

如果线程池定义在成员变量中,被多个方法调用,则不需要关闭线程池。

关闭线程池的方式有两种:

pool.shutdown();              // 优雅关闭,处理完队列任务
pool.shutdownNow();           // 立即关闭,尝试中断所有任务

pool.shutdown()会等待所有的任务处理完成后关闭。但是如果任务执行时间过长或者出现死循环,那么久无法关闭线程池了,那该如何解决呢?

// 等待线程终止:等待指定时间后,返回线程池是否任务是否执行完成
pool.awaitTermination(long timeout, TimeUnit unit)

当前方法需要再关闭线程池之后使用才能使用。为了保证线程池执行的时间,就可以使用上面的方法。

pool.shutdown();
// 100ms之后,如果还未关闭线程
if (!pool.awaitTermination(100, TimeUnit.MILLISECONDS)) {
    // 强制关闭线程
    pool.shutdownNow();
    System.out.println(DateUtil.now() + "线程池 终止了...");
};

3.2 拒绝策略

线程池任务饱和时,就会触发线程池的拒绝策略。

  • AbortPolicy(默认):抛出 RejectedExecutionException(强制失败快速暴露问题)
  • CallerRunsPolicy:由提交任务的线程执行(降级方案)
  • DiscardOldestPolicy:丢弃队列最旧任务(容忍部分丢失)
  • DiscardPolicy:静默丢弃新任务(慎用!)

当然还可以实现自己的拒绝策略。

3.3 提交任务

线程池提交任务的方式有两种:

// 不带返回值的任务
pool.execute();
// 带返回值的任务
pool.submit()

多线程处理任务的时候,大部分不带返回值。但是很多需要聚合多任务的结果时,就需要带返回值的任务提交。

3.4 线程池的参数配置

线程数时线程池的最不确定的因素,多线程的确可以提高响应速度,但是并不是越多越好,线程数过多,CPU频繁切换反而降低了响应的速度。

  • CPU密集型:线程数 ≈ CPU核心数(Runtime.getRuntime().availableProcessors()
  • IO密集型:线程数 ≈ CPU核心数 × (1 + 平均等待时间/计算时间)
  • 混合型:拆分任务分别使用不同线程池

上面的指导参数,只不过时理论的参数。现实的参数需要不断的调优,才能达到理想的状态。而线程池的监控必不可少,如DynamicTp

3.5 扩展

JDK8以后引入了线程编排工具java.util.concurrent.CompletableFuture,它本身也采用了线程池的方式。

CompletableFuture.runAsync(() -> System.out.println("测试"));

这里使用了ForkJoinPool线程池。

04 小结

线程池的核心优势就是降低资源的开销,线程复用减少创建和销毁的成本,各线程之间异步处理,提高响应速度。避免盲目使用 Executors 快捷方法,根据场景精细配置参数才能发挥最大效益!掌握线程池原理与实践,是构建高并发、高可靠系统的必备技能。合理使用这一利器,可让你的应用在流量洪峰中稳如磐石。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值