当前位置: 首页 > article >正文

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

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

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

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

什么是线程池

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

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

线程池状态

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

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

  • RUNNING:会接收新任务并且会处理队列中的任务

  • SHUTDOWN:不会接收新任务并且会处理队列中的任务

  • STOP:不会接收新任务并且不会处理队列中的任务,并且会中断在处理的任务(注意:一个任务能不能被中断得看任务本身)

  • TIDYING:所有任务都终止了,线程池中也没有线程了,这样线程池的状态就会转为 TIDYING,一旦达到此状态,就会调用线程池的 terminated()

  • TERMINATED:terminated()执行完之后就会转变为 TERMINATED

59bf6778684a2f657b18f5b42f3ff654.png

线程池工作原理

如何自定义一个线程池?

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");
        });
    }

线程池执行流程图

fb5a36bb159b61f13ee79b4bec13fdff.png
  1. 首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务。

  2. 如果当前线程数未超过核心线程数,则创建并启动一个线程来执行新提交的任务。

  3. 如果当前线程数超过核心线程数,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

  4. 如果当前线程数超过核心线程数且 线程池内的阻塞队列已满,且未超过最大线程数,则创建并启动一个线程来执行新提交的任务。

  5. 如果已超过最大线程数,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

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

线程池的拒绝策略

3692e3280f001534f0aa48bf1e23a79e.png

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

  1. CallerRunsPolicy,由调用 execute 方法提交任务的线程来执行这个任务。

  2. AbortPolicy,抛出异常 RejectedExecutionException 拒绝提交任务。

  3. DiscardPolicy,直接抛弃任务,不做任何处理。

  4. DiscardOldestPolicy,去除任务队列中的第一个任务(最旧的),重新提。

如何监控线程池?

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

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

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

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

411f88e5119262a07662890b456bbe53.png

监控核心代码

@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:线程发生异常,会被移出线程池吗?

以上,就是今天的分享,希望对大家有帮助。


最后介绍下我的《Java 面试高手心法 58 讲》专栏内容涵盖 Java 基础、Java 高级进阶、Redis、MySQL、消息中间件、微服务架构设计等面试必考点、面试高频点。

本专栏不会单纯教你背八股文知识,而是结合实际大厂高并发项目的场景,提取码哥多年的工作经验和面试经验,每篇文章平均画了 4 张图,让你高效的学习面试技术要点,掌握互联网 Java 流行技术体系要点难点,让你系统化的提升技术,做到事半功倍,拿下高薪 Offer。

丢掉你收藏的那些所谓的「面试宝典」,因为它们大多数深度不够,甚至内容还有错误,这也是为何每次面试你都回答不好的原因,你只会看完就忘,还浪费时间。

1061dbc8c82b302fb14d21d539e81ecc.png

🧧 截止到本周,还有双 11 限时特惠,欢迎感兴趣的同学加入体验,限时 9 折优惠券,用完为止,机不可失,大家抓住机会上车!

27fa8dc3c303c26a116b99eaa04a2b2f.png

往期推荐

一文讲透数据库与 Redis 缓存一致性问题

我的新书出版后,小伙伴们问是不是赚翻了?

今天面试了一个候选人,当场想给他 Offer

MySQL 索引下推,这个点你可能真不知道!

JVM 最全夺命连环 10 问,建议收藏!


http://www.kler.cn/a/389096.html

相关文章:

  • 【问题】Chrome安装不受支持的扩展 解决方案
  • Langchain+文心一言调用
  • 没有屋檐的房子-023粪堆旁边的舞蹈
  • Java 设计模式 二 单例模式 (Singleton Pattern)
  • RabbitMQ 高级特性
  • Android系统开发(十五):从 60Hz 到 120Hz,多刷新率进化简史
  • 【python】路径与文件管理:pathlib库的现代用法
  • 【WRF后处理】基于wrf-python处理wrf运行结果wrfout_d01
  • Linux:基本开发工具
  • 【go从零单排】Rate Limiting限流
  • 成都爱尔小儿眼科及视光团队多人当选“近视防控专家委员会委员”
  • CSS3_3D变换(七)
  • Vue CLI 脚手架
  • ubuntu 22.04 防火墙 ufw
  • imu_tk配置教程(锁死ubuntu18.04,不要22.04)
  • Spark的yarn集群环境搭建
  • C++ OpenCV 理想滤波
  • 挖掘web程序中的OAuth漏洞:利用redirect_uri和state参数接管账户
  • linux centos 安装redis
  • Qt_day4_Qt_UI设计
  • 骨传导耳机排行榜前十分享:十大超值骨传导耳机测评推荐!
  • NoSQL大数据存储技术测试(3)Hadoop和HBase简介
  • AI产品经理:新兴行业的新宠儿,站在风口上的猪都能飞上天
  • UI组件---如何设置el-pagination分页组件的背景色
  • 13. Node.js会话控制
  • Redis穿透、击穿、雪崩