小米面试:什么是线程池?工作原理是什么?线程池可以动态修改吗?

开发 前端
线程池(Thread Pool)是一种基于池化思想管理线程的工具,它维护多个线程。在线程池中,总有几个活跃线程。当需要使用线程来执行任务时,可以从池子中随便拿一个空闲线程来用,当完成工作时,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

大家好,我是码哥,《Redis 高手心法》畅销书作者。

有读者分享小米 Java 后端面试,其中有一个问题,当时没有回答好:什么是线程池、工作原理是什么、线程池可以动态修改吗?

回答这个问题之前,首先我们来了解下什么是线程池,它的工作原理是什么。

什么是线程池

线程池(Thread Pool)是一种基于池化思想管理线程的工具,它维护多个线程。在线程池中,总有几个活跃线程。当需要使用线程来执行任务时,可以从池子中随便拿一个空闲线程来用,当完成工作时,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

线程池状态

然后,我们来看下线程池有哪些状态呢?

线程池有五种状态:这五种状态并不能任意转换,只会有以下几种转换情况:线程池的五种状态是如何流转的?

  • RUNNING:会接收新任务并且会处理队列中的任务
  • SHUTDOWN:不会接收新任务并且会处理队列中的任务
  • STOP:不会接收新任务并且不会处理队列中的任务,并且会中断在处理的任务(注意:一个任务能不能被中断得看任务本身)
  • TIDYING:所有任务都终止了,线程池中也没有线程了,这样线程池的状态就会转为 TIDYING,一旦达到此状态,就会调用线程池的 terminated()
  • TERMINATED:terminated()执行完之后就会转变为 TERMINATED

图片

线程池工作原理

如何自定义一个线程池?

public ThreadPoolExecutor threadPoolExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                // 核心线程池大小,表示线程池常驻线程数量
                30,
                // 最大线程数,表示线程池最多创建的线程数量
                100,
                // 保活时间,表示一个非核心线程多久没有使用,会被回收
                10,
                TimeUnit.MINUTES,
                // 阻塞队列,表示队列最多缓存多少任务,如果队列满了,将触发 RejectedExecutionHandler
                new ArrayBlockingQueue<>(1000),
                // 线程工厂,创建线程时候用的,可以给线程命名等
                new NamedThreadFactory("cust-task")
        );
        // 拒绝策略,当阻塞队列满了之后,会触发这里的handler
        // 默认是丢弃新任务
        executor.setRejectedExecutionHandler((r, executor1) -> {
            log.warn("thread pool is full");
        });
    }

线程池执行流程图

图片

  1. 首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务。
  2. 如果当前线程数未超过核心线程数,则创建并启动一个线程来执行新提交的任务。
  3. 如果当前线程数超过核心线程数,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果当前线程数超过核心线程数且 线程池内的阻塞队列已满,且未超过最大线程数,则创建并启动一个线程来执行新提交的任务。
  5. 如果已超过最大线程数,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

注意:提交一个 Runnable 时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程。

线程池的拒绝策略

图片

ThreadPoolExecutor 内部有实现 4 个拒绝策略:

  1. CallerRunsPolicy,由调用 execute 方法提交任务的线程来执行这个任务。
  2. AbortPolicy,抛出异常 RejectedExecutionException 拒绝提交任务。
  3. DiscardPolicy,直接抛弃任务,不做任何处理。
  4. DiscardOldestPolicy,去除任务队列中的第一个任务(最旧的),重新提。

如何监控线程池?

好了,言归正传,再回归到这个题目本身,在修改线程池之前,我们要如何监控线程池的信息呢?

比如线程池的执行任务前后总时间,当前任务数等信息。

  • 统计任务执行时间可以通过实现 beforeExecute 和 afterExecute 方法,计算出任务总耗时。

图片

  • 统计线程池的任务数,线程数等信息,可定时上报到 kafka,展示到可视化的界面上比如 Grafana。

图片

监控核心代码

@Slf4j
public class ThreadPoolMonitor {

    private final ThreadPoolExecutor customThreadPool;
    private final String poolName;
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    public ThreadPoolMonitor(ThreadPoolExecutor customThreadPool, String poolName) {
        this.customThreadPool = customThreadPool;
        this.poolName = poolName;
    }

    public void startMonitoring(long period, TimeUnit unit) {
        scheduler.scheduleAtFixedRate(this::monitor, 0, period, unit);
    }

    private void monitor() {
        //核心线程数
        int corePoolSize = customThreadPool.getCorePoolSize();
        //最大线程数
        int maximumPoolSize = customThreadPool.getMaximumPoolSize();
        //活跃线程数
        int activeCount = customThreadPool.getActiveCount();
        //队列任务数
        int queueSize = customThreadPool.getQueue().size();
        //已执行完成任务数
        long completedTaskCount = customThreadPool.getCompletedTaskCount();
        //队列任务数峰值
        int largestPoolSize = customThreadPool.getLargestPoolSize();

        //上报监控数据
        sendToKafka(corePoolSize,maximumPoolSize, activeCount, queueSize, completedTaskCount, largestPoolSize);
    }

    private void sendToKafka(int corePoolSize,int maximumPoolSize, int activeCount, int queueSize, long completedTaskCount, int largestPoolSize) {
        // 自定义实现发送kafka逻辑或上报到prometheus逻辑
    }
}

如何动态调整线程池?

一般我们在设置线程池的线程数时,会参考实际业务场景。比较通用的公式是

  • IO 密集型场景:线程数=CPU 核心数*2+1
  • CPU 密集型场景线程数=CPU 核心数+1

但这只是比较简单粗暴的计算方式,在实际使用过程中,我们还是不可避免的需要调整线程池的一些参数,以达到最佳性能。

那么我们通过会比较关注线程池以下的几个参数

线程池参数

说明

corePoolSize

核心线程数

maximumPoolSize

最大线程数

queueCapacity

等待队列大小

keepAliveTime

空闲时间

  1. corePoolSize、maximumPoolSize 和 keepAliveTime 可以通过调用 setCorePoolSize、setMaximumPoolSize、setKeepAliveTime 方法修改。
  2. queueCapacity 虽然不能直接修改,我们可以通过实现自定义一个阻塞队列的方式去实现 setQueueCapacity 方法来修改队列大小的属性。

最后可以通过 Apollo、Nacos 配置中心实现动态监听的方法,达到实时更新线程池的效果。

扩展 1:线程池核心线程数会被销毁吗?

扩展 2:线程发生异常,会被移出线程池吗?

责任编辑:武晓燕 来源: 码哥跳动
相关推荐

2024-05-20 10:03:15

线程池优先级队列排序方法

2024-07-15 08:20:24

2022-03-02 07:36:37

池化技术Java线程池

2024-03-11 18:18:58

项目Spring线程池

2013-08-27 14:04:29

2021-07-16 11:35:20

Java线程池代码

2022-09-13 07:50:26

小米面试官MySQL

2020-12-10 08:24:40

线程池线程方法

2012-05-15 02:18:31

Java线程池

2021-02-05 12:34:33

线程池系统

2020-03-05 15:34:16

线程池C语言局域网

2022-06-24 06:43:57

线程池线程复用

2020-04-29 14:10:44

Java线程池编程语言

2022-03-21 07:40:08

线程池Executors方式

2022-03-14 08:02:08

轻量级动态线程池

2023-05-19 08:01:24

Key消费场景

2023-07-28 07:18:39

final继承结构

2023-01-29 08:04:24

线程池非核心线程任务

2023-12-20 14:35:37

Java虚拟线程

2019-11-13 14:38:34

Executors阿里线程池
点赞
收藏

51CTO技术栈公众号