网上资料千篇一律面试题,聊一下我在工作中用线程池的经验。
线程池的分类
业务分类
异步任务
比如订单事件,把非主流程的逻辑,放到异步事件里面,或者用异步线程执行。
延时任务
常见的延迟一会执行,比如等待数据库事务提交,比如回调业务失败,过 3s 重试。
大型任务
比如大型的定时任务,执行时间比较久的,工作量比较大的。
技术选型
Spring 的 ThreadPoolTaskExecutor
这是 Spring 的线程池,和 @EnableAsync 和 @Async 搭配使用很方便。
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(5);
// 最大线程数
executor.setMaxPoolSize(10);
// 队列容量
executor.setQueueCapacity(100);
// 线程存活时间
executor.setKeepAliveTime(60, TimeUnit.SECONDS);
// 线程名称前缀
executor.setThreadNamePrefix("MyThreadPool-");
// 拒绝策略,这里使用 CallerRunsPolicy
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 允许核心线程超时
executor.setAllowCoreThreadTimeOut(false);
// 初始化线程池
executor.initialize();
return executor;
}
简单说一下配置参数和含义。
- 核心线程数(Core Pool Size)
方法:executor.setCorePoolSize(int corePoolSize)
含义:常驻的核心线程数。 - 最大线程数(Max Pool Size)
方法:executor.setMaxPoolSize(int maxPoolSize)
含义: 允许的最大线程数。 - 队列容量(Queue Capacity)
方法:等待执行任务的最大容量。 - 线程存活时间(Keep Alive Time)
方法:executor.setKeepAliveTime(long keepAliveTime, TimeUnit unit)
含义: 超出常驻线程数的线程,无任务后多久销毁。 - 线程工厂(Thread Factory)
方法:executor.setThreadFactory(ThreadFactory threadFactory)
含义: 默认就行。 - 拒绝策略(Rejected Execution Handler)
方法:executor.setRejectedExecutionHandler(RejectedExecutionHandler handler)
含义: 当线程池和队列都已满,无法处理新提交的任务时,会执行的策略。
常见的拒绝策略有: ThreadPoolExecutor.AbortPolicy:直接抛出 RejectedExecutionException,表示任务被拒绝。 ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)执行该任务,降低新任务的提交速度。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试再次提交新任务。 ThreadPoolExecutor.DiscardPolicy:直接丢弃新提交的任务,不抛出异常。 - 线程名称前缀(Thread Name Prefix)
方法:executor.setThreadNamePrefix(String threadNamePrefix)
含义:线程名字前缀,打日志用。 - 等待任务完成时的关闭超时时间(Await Termination Timeout)
方法:executor.setAwaitTerminationSeconds(int seconds)
含义:关闭线程池的最大等待执行时间,不会用,没人会主动关闭线程池。 - 允许核心线程超时(Allow Core Thread Timeout)
方法:executor.setAllowCoreThreadTimeOut(boolean value)
含义: 设置为 true 时,核心线程在空闲时间达到 keepAliveTime 后也会被终止,否则核心线程会一直保持活跃。
ForkJoinPool.commonPool()
这是 Java 8 stream 并行流(ParallelStream
,lambda 里面的 map 呀、foreach 等操作)和 CompletableFuture
(异步编程) 的默认线程池,他的核心线程数固定为 CPU 数量 - 1,最小为 1,并且无法修改默认值。
import java.util.Arrays;
import java.util.List;
public class StreamParallelExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> result = numbers.parallelStream()
.map(n -> {
System.out.println(Thread.currentThread().getName());
return n * 2;
})
.toList();
System.out.println(result);
}
}
特点是工作窃取算法(Work-Stealing Algorithm),可以在线程空闲时,从别的线程队列底部拉取任务执行。
我们 k8s 配的服务资源是 1 核 2G,这就尴尬了,线程池只有 1 个线程,如果我要用异步编程,得单独传入线程池。
配置技巧
配置的时候注意下内存,一个活跃线程的栈空间最大 1M,也就是最大线程数要注意下,避免 OOM。
其他队列、指针不占用,10 M 都很够了,1000 个队列才 1M 多。
根据业务区分下不同的线程池,比如大型任务,线程池数量少一点,并发不要太大,防止 CPU 100%。
避免线程频繁的上下文切换,反而会降低执行效率。