5
头图
基本上所有的程序员都直接或间接使用过线程池。它是一把双刃剑,用好了事半功倍,用不好则可能直接打崩服务器。所以我们要对线程池有所了解,才能更好的去使用它。当然主要还是基于Java及SpringBoot。

1 线程池

1.1 为什么使用线程池

  • 线程的创建和销毁是比较耗时的,利用池化技术可以减少这些消耗。
  • 线程池一般是要维护几个核心线程等待任务去执行的,这样可以提高响应速度。
  • 同时使用线程池也可以更方便的对线程进行管理,如控制数量、监控执行状态等。

1.2 如何创建线程池

在Java的juc包中,有提供的Executors类。一般情况为了使用方便,可能会直接使用该类的静态方法进行创建。而在这些静态方法中,都是基于ThreadPoolExecutor类来构造的。下面我们分别来看一下。

1.2.1 ThreadPoolExecutor

线程池的重中之重,我们需要对它的各个参数和整体流程都十分熟悉才行。关于源码解析,我们后面单独用一篇博客来讲解。下面来看它参数最全的一个构造方法。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

参数解释:

  • corePoolSize: 核心线程数。在线程池中要保持的线程数量,即使它是空闲的。除非设置allowCoreThreadTimeOut否则不会减少。可以调用prestartCoreThreadprestartAllCoreThreads来预先创建一个或全部核心线程。
  • maximumPoolSize: 最大线程数。线程池中所允许的最大线程数量。
  • keepAliveTime, unit: 存活时间。当线程数大于核心线程数时,若线程空闲时间大于该值将会被终止。
  • workQueue: 工作队列。阻塞队列,存储当前尚未被执行的任务。

    • ArrayBlockingQueue: 基于数组的有界阻塞队列。
    • LinkedBlockingQueue: 基于链表的阻塞队列,吞吐量通常高于ArrayBlockingQueue。
    • SynchronousQueue: 不存储元素的阻塞队列,每个插入必须等待另一个线程调用移除操作
    • PriorityBlockingQueue: 具有优先级的无限阻塞队列。
  • threadFactory: 线程工厂。用于生成线程。主要设置是线程名称前缀,用于区分。这块一般会使用Google提供的guava包中的com.google.common.util.concurrent.ThreadFactoryBuilder来构建。
  • handler: 拒绝策略。当当前线程池达到最大处理量时,则执行拒绝策略。这个其实就是标准的策略模式

    • CallerRunsPolicy: 当前exector未关闭的情况下,由调用线程执行被拒绝的任务。
    • AbortPolicy: 拒绝当前任务执行,并抛出RejectedExecutionException异常。
    • DiscardPolicy: 直接丢弃当前任务,啥也不做。
    • DiscardOldestPolicy: 当前exector未关闭的情况下,丢弃队列中最早的那个任务。

整体执行流程:

线程池-整体执行流程-20221206.png

1.2.2 Executors内置静态方法
FixedThreadPool
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

特点:

  • 核心线程数与最大线程数相同
  • 存活时间:0。因为核心线程数和最大线程数相同,该参数无意义
  • 阻塞队列为LinkedBlockingQueue可以认为是无界队列

当线程池线程数量达到最大线程数后,会将后续任务一直向阻塞队列压入。可能会触发OOM异常。

SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

特点:

  • 几乎等同于newFixedThreadPool(1)
  • 实际返回为FinalizableDelegatedExecutorService
private static class FinalizableDelegatedExecutorService
        extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
        super(executor);
    }
    @SuppressWarnings("deprecation")
    protected void finalize() {
        super.shutdown();
    }
}

可见在finalize()方法内,主动调用了super.shutdown()。也就是当该执行器被垃圾回收时,会主动对实际执行器进行关闭。但还是建议当不使用时,及时主动关闭。

因为使用了LinkedBlockingQueue可能会触发OOM异常。

newCachedThreadPool
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

特点:

  • 核心线程数为0,最大线程数∞(可以认为是无穷大)
  • 存活时间60秒
  • 阻塞队列为SynchronousQueue,容量为0

每个提交的任务都会立即获得一个线程来执行,这样可能会造成大量的线程,从而触发OOM异常。

ScheduledThreadPool, SingleThreadScheduledExecutor
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1, threadFactory));
}

继承于ThreadPoolExecutor扩展了其功能,可以使提交的任务延迟一定时间执行,或者周期执行。

WorkStealingPool
public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

在1.8中引入,使用ForkJoinPool做为实现。该线程池会维持足够多的线程来支持给定的并行级别,可以使用多个队列来减少竞争。线程的实际数量可以动态的增加或减少。不能保证提交任务的执行顺序。

1.2.3 线程池提交任务
  • execute: 常用的提交方式无返回值。
  • submit: 适合需要有返回值的任务提交。会返回一个Future对象,可通过该对象判断任务是否执行并获取任务执行结果。

Future在1.5中引入,在1.8中又引入了CompletableFuture,关于CompletableFuture后续再单独一篇讲一下。

1.2.4 线程池的关闭
  • shutdownNow: 将当前线程池置为STOP状态,然后停止所有当前正在执行、暂停的任务,并返回任务待执行列表。
  • shutdown: 将当前线程置为SHUTDOWN状态,中断没有在执行任务的线程,不再接受新任务,但之前提交的任务都会被执行。

1.3 线程池的监控

线程池本身提供了很多属性供获取:

  • getPoolSize:线程池当前线程数
  • getCorePoolSize:线程池核心线程数
  • getActiveCount:正在执行任务的线程数量
  • getCompletedTaskCount:已完成任务的大致总数 (任务和线程的状态可能在计算过程中有所变化)
  • getTaskCount:全部任务的大致总数
  • getQueue:当前线程池的任务队列
  • getLargestPoolSize:线程池曾经最大线程数量
  • getMaximumPoolSize:线程池允许最大线程数
  • getKeepAliveTime:线程池线程存活时间
  • isShutdown:线程池是否为关闭 (SHUTDOWN 状态)
  • isTerminated:线程池是否为 TERMINATED 状态

同时线程池也提供了几个protected空实现的方法。可以通过继承方式,重写beforeExecute(Thread t, Runnable r)afterExecute(Runnable r, Throwable t)terminated()在扩展点出获取线程池状态。

1.4 配合Prometheus暴露监控指标

本例中的线程池,主要目的是想提高并发,但是并不想任务出现延迟,如果当前没有空闲线程来执行任务,则由自己去执行。

整体逻辑很简单,内部启用一个SingleThreadScheduledExecutor每隔一定时间,收集线程池数据并通过io.micrometer.core.instrument.Metrics对外暴露。

@Slf4j
@EnableAsync
@Configuration
public class ExecutorConfig implements InitializingBean, DisposableBean {
    public static final String THREAD_POOL_NAME_FUTURE = "futureExecutor";

    private static final ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
    
    private static final Iterable<Tag> TAG = Collections.singletonList(Tag.of("thread.pool.name", THREAD_POOL_NAME_FUTURE));

    @Bean(name = THREAD_POOL_NAME_FUTURE)
    public Executor futureExecutor() {
        return new ThreadPoolExecutor(
                20,
                80,
                10,
                TimeUnit.MINUTES,
                new SynchronousQueue<>(),
                new ThreadFactoryBuilder().setNameFormat(THREAD_POOL_NAME_FUTURE + "-%d").build(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    @Override
    public void afterPropertiesSet() {
        scheduledExecutor.scheduleAtFixedRate(doCollect(), 30L, 5L, TimeUnit.SECONDS);
    }

    @Override
    public void destroy() {
        try {
            scheduledExecutor.shutdown();
        } catch (Exception ignored) {}
    }

    private Runnable doCollect() {
        return () -> {
            try {
                ThreadPoolTaskExecutor exec = (ThreadPoolTaskExecutor) futureExecutor();
                Metrics.gauge("thread.pool.core.size", TAG, exec, ThreadPoolTaskExecutor::getCorePoolSize);
                Metrics.gauge("thread.pool.max.size", TAG, exec, ThreadPoolTaskExecutor::getMaxPoolSize);
                Metrics.gauge("thread.pool.keepalive.seconds", TAG, exec, ThreadPoolTaskExecutor::getKeepAliveSeconds);
                //
                Metrics.gauge("thread.pool.active.size", TAG, exec, ThreadPoolTaskExecutor::getActiveCount);
                Metrics.gauge("thread.pool.thread.count", TAG, exec, ThreadPoolTaskExecutor::getPoolSize);
                //
                Metrics.gauge("thread.pool.queue.size", TAG, exec, e -> e.getThreadPoolExecutor().getQueue().size());
                //
                Metrics.gauge("thread.pool.task.count", TAG, exec, e -> e.getThreadPoolExecutor().getTaskCount());
                Metrics.gauge("thread.pool.task.completed.count", TAG, exec, e -> e.getThreadPoolExecutor().getCompletedTaskCount());
            } catch (Exception ex) {
                log.warn("doCollect ex => {}", ex.getLocalizedMessage());
            }
        };
    }
}

1.5 SpringBoot中的线程池

在Spring项目中,经常会使用@EnableAsync@Async来实现异步任务。默认情况下,会使用ThreadPoolTaskExecutor

public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
        implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {

    private final Object poolSizeMonitor = new Object();

    private int corePoolSize = 1;

    private int maxPoolSize = Integer.MAX_VALUE;

    private int keepAliveSeconds = 60;

    private int queueCapacity = Integer.MAX_VALUE;

    private boolean allowCoreThreadTimeOut = false;

    private boolean prestartAllCoreThreads = false;

默认拒绝策略为ThreadPoolExecutor.AbortPolicy()。所以一般来说,我们都是需要自定义线程池的。

2 注意事项

2.1 阿里巴巴Java开发手册

  • 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

    • 这个很好理解,像http-nio-8103-exec-1这种,一眼就能看出是http web相关线程
  • 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

    • 参考线程池的好处
  • 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

2.2 其他

关于线程池中的,核心线程数、最大线程数、存活时间、及阻塞队列的选型及容量。没有啥固定的公式。

一般情况下, 会根据任务类型来做些区分

  • CPU密集型任务。这种一般将线程数设置小一些,线程过多则上下文切换可能会耗时占比比较高。
  • IO密集型任务。这种则尽量设置多一些的线程。可以大概预估请求等待时间 (WT) 和服务时间 (ST)之间的比例。线程池大小设置为 N*(1+WT/ST)。

不过最好还是做好指标监控,根据业务场景不断地调整优化。初始值可在测试环境等做些压测来确定。

3 后记

合理使用才能事半功倍,要始终抱着敬畏之心,防患于未然,做好指标监控。

又留俩坑:

  • [ ] ThreadPoolExecutor源码
  • [ ] CompletableFuture相关

echo '5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp' | base64 -d

lpe234
4.1k 声望2k 粉丝

路漫漫其修远兮