01 引言
Java
高并发的项目,少不了多线程的使用,而多线程的管理自然要使用到线程池。池化的思想是很多架构一定会考虑的要素,可以很好的控制资源的资源的使用,但是使用不好,造成的问题也不容小觑。
多线程的使用需要考虑的因素很多,这也成了面试八股文必问为技术点。
02 线程池的创建
你是否还在这样的创建线程池?
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
Executors.newFixedThreadPool(5);
Executors.newScheduledThreadPool(5);
这样封装好的线程池的创建方法,可以帮我们快速的创建线程池。在使用线程时,优先使用线程池去创建,阿里巴巴的技术规约强制使用线程池创建线程。
但是线程池里面的核心参数,我们是不清楚的,这也是同样是阿里巴巴的技术规约不允许使用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
快捷方法,根据场景精细配置参数才能发挥最大效益!掌握线程池原理与实践,是构建高并发、高可靠系统的必备技能。合理使用这一利器,可让你的应用在流量洪峰中稳如磐石。