线程池的使用场景

线程池整理汇总 中介绍了线程池的参数设置,其中涉及三个系统参数:每秒需要处理的任务数量处理一个任务所需要的时间系统允许任务最大的响应时间

应用场景分类

其实,总结下来应用场景一般分为两个维度:并发量任务执行时长。一般分为:

1. 高并发、任务执行时间短:
线程池线程数可以设置为少一些(以CPU核数+1为准),减少线程上下文的切换

2. 高并发、任务执行时间长:
解决这种类型任务的关键不在于线程池而在于整体架构的设计。

  1. 第一步:看看这些业务里面某些数据是否能做缓存
  2. 第二步:增加服务器
  3. 第三步:业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件(例mq)对任务进行拆分和解耦
  4. 第四步:使用线程池,将可预见的会发生阻塞操作的代码块部分放入线程池进行执行,如此这样,当主线程执行到线程池的部分,会执行线程池的run()方法,然后主线程会继续向下执行,而阻塞的部分将在run()方法中执行,不会阻塞主线程的执行,最后主线程一般要汇总各个子线程返回的结果,这样可以达到一个异步非阻塞的快速响应。

3. 并发不高、任务执行时间短
线程池线程数可以设置为少一些(以CPU核数+1为准),减少线程上下文的切换。

4. 并发不高、任务执行时间长
需要按任务区分,IO密集型任务CPU密集型任务

  1. IO密集型任务:任务时间长集中在IO操作上,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务。可以参考 线程池整理汇总 中介绍了线程池的参数设置
  2. CPU密集型任务:任务时间长集中在CPU计算上,线程池线程数可以设置为少一些(以CPU核数+1为准),减少线程上下文的切换。

线程池在业务中的实践

转自美团Java线程池实现原理及其在美团业务中的实践

1 业务背景

场景1:快速响应用户请求(IO密集型任务)

描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户所以应该不设置队列去缓冲并发任务,调高 corePoolSize 和 maxPoolSize 去尽可能创造多的线程快速执行任务。

在这里插入图片描述

场景2:快速处理批量任务(CPU密集型任务)

描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。

分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的 corePoolSize 去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

在这里插入图片描述


2 实际问题及方案思考

线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。

我们是否可以将修改线程池参数的成本降下来,这样至少可以发生故障的时候可以快速调整从而缩短故障恢复的时间呢?基于这个思考,我们是否可以将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化前后的参数修改流程对比如下:
在这里插入图片描述


3 动态化线程池

3.1 整体设计

动态化线程池的核心设计包括以下三个方面:

  1. 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略。考虑到在实际应用中我们获取并发性的场景主要是两种:(1)并行执行子任务,提高响应速度(IO密集型任务)。这种情况下,应该使用同步队列,没有什么任务应该被缓存下来,而是应该立即执行。(2)并行执行大批次任务,提升吞吐量(CPU密集型任务)。这种情况下,应该使用有界队列,使用队列去缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More。
  2. 参数可动态修改:为了解决参数不好配,修改参数成本高等问题。在 Java 线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。
  3. 增加线程池监控:对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态。

在这里插入图片描述

3.2 功能架构

动态化线程池提供如下功能:

  • 动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。
  • 任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。
  • 负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。
  • 操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。
  • 操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。
  • 权限校验:只有应用开发负责人才能够修改应用的线程池参数。

在这里插入图片描述

参数动态化
JDK原生线程池ThreadPoolExecutor提供了如下几个 public 的 setter 方法,如下图所示:
在这里插入图片描述

JDK 允许线程池使用方通过 ThreadPoolExecutor 的实例来动态设置线程池的核心策略,以setCorePoolSize为方法例,在运行期线程池使用方调用此方法设置 corePoolSize 之后,线程池会直接覆盖原来的 corePoolSize 值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的 worker 线程,此时会向当前 idle 的 worker 线程发起中断请求以实现回收,多余的 worker 在下次 idel 的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的 worker 线程来执行队列任务。

重点是基于这几个public方法,我们只需要维护ThreadPoolExecutor的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,我们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示:
在这里插入图片描述
用户可以在管理平台上通过线程池的名字找到指定的线程池,然后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此之外,在界面中,我们还能看到用户可以配置是否开启告警、队列等待任务告警阈值、活跃度告警等等。关于监控和告警,我们下面一节会对齐进行介绍。

线程池监控
除了参数动态化之外,为了更好地使用线程池,我们需要对线程池的运行状况有感知,比如当前线程池的负载是怎么样的?分配的资源够不够用?任务的执行情况是怎么样的?是长任务还是短任务?基于对这些问题的思考,动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等,既能帮助用户从多个维度分析线程池的使用情况,又能在出现问题第一时间通知到用户,从而避免故障或加速故障恢复。

用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,可以读取到当前线程池的运行状态以及参数,如下图所示:
在这里插入图片描述

动态化线程池基于这几个接口封装了运行时状态实时查看的功能,用户基于这个功能可以了解线程池的实时状态,比如当前有多少个工作线程,执行了多少个任务,队列中等待的任务数等等。


实际代码

package 线程池;

import java.util.concurrent.*;

public class ThreadPollDemo {
    public static void main(String[] args) {
        /* 创建线程池 */
        int corePoolSize = 5;
        int maximumPoolSize = 10;
        long keepAliveTime = 60;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new SynchronousQueue<>();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        /* 创建任务 */
        Runnable task = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("+++");
            }
        };
        /* 执行任务 */
        pool.submit(task);
        /* 打印线程池参数 */
        printThreadPollInfo(pool);

        System.out.println("+++线程池参数修改后+++");
        pool.setCorePoolSize(10);
        pool.setMaximumPoolSize(20);

        printThreadPollInfo(pool);
    }

    public static void printThreadPollInfo(ThreadPoolExecutor pool) {
        System.out.println(pool.getCorePoolSize());
        System.out.println(pool.getMaximumPoolSize());
        System.out.println(pool.getQueue());
        System.out.println(pool.getActiveCount());
    }
}

在这里插入图片描述

结果可见,线程池参数动态修改成功。

本实践中只实现了前两个参数的动态修改,因为 JDK 自带的 ThreadPoolExecutor 并没有对外提供设置 workQueue 及其容量大小的方法

如果确实要实现动态调整工作队列,我们可以通过 ThreadPoolExecutor 提供的getQueue()方法获取到工作队列对象实例然后修改其容量,但是默认 BlockingQueue 一旦创建就没法修改其容量大小,所以我们需要自己实现一个可以动态调整容量大小的队列即可。

引用文章

Java线程池实现原理及其在美团业务中的实践
线程池详解(通俗易懂超级好)

  • 2
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
线程池是一种管理和复用线程的机制,可以提高线程的利用率和系统的性能。下面是线程池常见的使用场景和相应的辅助类: 1. 并行计算:当需要对大量数据进行并行计算时,可以使用线程池来提供并发执行的线程。在这种场景下,可以使用 CountDownLatch 来等待所有线程完成计算任务。 2. 批量任务处理:当需要处理大量的任务时,可以使用线程池来管理任务的执行。通过将任务提交给线程池线程池会自动分配和管理线程来执行这些任务。在这种场景下,可以使用 Future 来获取任务的执行结果。 3. 异步操作:当需要执行一些耗时的异步操作时,可以使用线程池来处理。通过将异步任务提交给线程池,可以避免阻塞主线程,提高程序的响应性。在这种场景下,可以使用 Future 来获取异步操作的执行结果。 4. 定时任务:当需要执行定时任务时,可以使用线程池来定时调度任务的执行。通过使用 ScheduledThreadPoolExecutor 类,可以创建一个定时任务线程池,用于按照指定的时间间隔或时间点执行任务。 5. 长时间运行的后台任务:当需要执行一些长时间运行的后台任务时,可以使用线程池来管理这些任务的执行。通过使用 FixedThreadPool 或 CachedThreadPool 类,可以创建一个适用于长时间运行的后台任务的线程池。 这些辅助类(如 CountDownLatch 和 Future)可以与线程池结合使用,以实现更灵活和高效的多线程编程。它们提供了不同的功能,如等待线程完成、获取线程执行结果等,可以根据具体需求选择合适的辅助类来实现多线程操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值