Java 线程池经验

978 阅读3分钟

网上资料千篇一律面试题,聊一下我在工作中用线程池的经验。

线程池的分类

业务分类

异步任务

比如订单事件,把非主流程的逻辑,放到异步事件里面,或者用异步线程执行。

延时任务

常见的延迟一会执行,比如等待数据库事务提交,比如回调业务失败,过 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;
    }

简单说一下配置参数和含义。

  1. 核心线程数(Core Pool Size)
    方法:executor.setCorePoolSize(int corePoolSize)
    含义:常驻的核心线程数。
  2. 最大线程数(Max Pool Size)
    方法:executor.setMaxPoolSize(int maxPoolSize)
    含义: 允许的最大线程数。
  3. 队列容量(Queue Capacity)
    方法:等待执行任务的最大容量。
  4. 线程存活时间(Keep Alive Time)
    方法:executor.setKeepAliveTime(long keepAliveTime, TimeUnit unit)
    含义: 超出常驻线程数的线程,无任务后多久销毁。
  5. 线程工厂(Thread Factory)
    方法:executor.setThreadFactory(ThreadFactory threadFactory)
    含义: 默认就行。
  6. 拒绝策略(Rejected Execution Handler)
    方法:executor.setRejectedExecutionHandler(RejectedExecutionHandler handler)
    含义: 当线程池和队列都已满,无法处理新提交的任务时,会执行的策略。
    常见的拒绝策略有: ThreadPoolExecutor.AbortPolicy:直接抛出 RejectedExecutionException,表示任务被拒绝。 ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)执行该任务,降低新任务的提交速度。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试再次提交新任务。 ThreadPoolExecutor.DiscardPolicy:直接丢弃新提交的任务,不抛出异常。
  7. 线程名称前缀(Thread Name Prefix)
    方法:executor.setThreadNamePrefix(String threadNamePrefix)
    含义:线程名字前缀,打日志用。
  8. 等待任务完成时的关闭超时时间(Await Termination Timeout)
    方法:executor.setAwaitTerminationSeconds(int seconds)
    含义:关闭线程池的最大等待执行时间,不会用,没人会主动关闭线程池。
  9. 允许核心线程超时(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%。

避免线程频繁的上下文切换,反而会降低执行效率。