什么?竟然还有人不知道线程池预热?

阅读本文前,最好具备线程池相关的知识,点击以下文章即可跳转阅读。

万字长文深度解读Java线程池,硬核源码分析

背景

并发任务处理面临着一些挑战和需求,以下是其中一些常见的挑战和需求:

  1. 1. 响应时间要求:在高并发场景下,任务的响应时间非常关键。系统需要能够快速接收、处理和响应大量的并发任务,以满足用户的实时需求。

  2. 2. 资源管理:高并发任务处理需要有效地管理系统资源,包括CPU、内存、网络等。合理分配和利用资源,避免资源瓶颈和浪费,以提供高效的任务处理能力。

  3. 3. 吞吐量要求:在高并发场景下,系统需要具备高吞吐量的能力,即能够同时处理大量的并发任务。任务的处理速度和并发能力是关键指标,需要通过合理的架构优化手段来提高系统的吞吐量。

  4. 4. 扩展性和弹性:高并发任务处理需要具备良好的扩展性和弹性,能够根据负载情况自动进行水平扩展或缩减。系统需要能够动态调整资源和处理能力,以适应不断变化的并发需求。

  5. 5. 优化性能:高并发任务处理需要不断优化系统性能,包括减少任务处理的延迟、提高资源利用率、优化算法和数据结构等。通过性能调优,可以提升系统的响应速度和并发处理能力。

所以通常在处理高并发任务时,除了硬件资源外,我们还可以尽可能的提高软件系统的处理能力比如采用消息队列、异步处理、线程池等技术来优化性能。

什么是线程池预热

先提一下线程池,线程池是一组预先创建的线程,可以重复使用来执行并发任务。线程池的好处在于避免了频繁创建和销毁线程的开销,提高了系统的效率和资源利用率。然而,如果在线程池中的线程首次被使用时才创建,可能会有一定的延迟,影响系统的响应时间。为了解决这个问题,可以在系统启动或负载增加之前预先创建和准备线程池中的线程,这个过程称为线程池预热,也是一种优化技术。预热过程通常在系统初始化阶段或低负载期间进行。通过预先创建线程,线程池可以更快地响应任务请求,减少任务等待时间,以提高系统的响应性能和吞吐量。

线程池预热的实现方式

创建线程池后,我们可以通过以下两种方法进行预热。

使用线程池提供的方法

线程池自带了两个预热线程的方法,看一下ThreadPoolExecutor这个类的方法,prestartCoreThread方法用于启动一个核心线程,prestartAllCoreThreads方法用于启动所有线程(也就是创建线程池时设置的maximumPoolSize)。

  • • prestartCoreThread

启动一个核心线程,等待任务队列中有任务到达就执行,启动核心线程成功则返回true,如果启动核心线程失败或者线程池中的线程数已经达到设置的核心线程池corePoolSize则返回false。

public boolean prestartCoreThread() {
    return workerCountOf(ctl.get()) < corePoolSize &&
        addWorker(nulltrue);
}

此方法先通过workerCountOf获取线程池的线程数,如果大于等于corePoolSize则直接返回false,小于核心线程数时才去执行addWorker方法,addWorker的第二个参数为true,表示此时要创建的就是核心线程,创建并启动线程成功则返回true。

  • • prestartAllCoreThreads

启动所有核心线程,返回启动的线程数。采用while循环的方式,判断调用addWorker方法的返回结果,addWorker的第二个参数为true,表示此时要创建的就是核心线程,创建并启动线程成功则返回true,直到addWorker返回false则不再继续,返回已启动的线程数(注意:并不一定等于corePoolSize)。

public int prestartAllCoreThreads() {
    int n = 0;
    while (addWorker(nulltrue))
        ++n;
    return n;
}

主动提交任务

在创建完线程池后,通过调用submit方法或者execute方法提交无逻辑的空任务到线程池使得工作线程得以创建并等待任务。这种方式相比线程池自带的两个预热方法就灵活很多,开发者可根据实际情况创建去预热指定数量的线程(如果提交的任务数都超过队列的大小时会创建非核心线程,直到达到maximumPoolSize,但是这样好像也没有必要,一般也不会这么做,预热核心线程即可)。

通过以上两种方式,线程池就可以在系统启动时预先创建和初始化一定数量的线程,从而减少任务到来时线程创建和初始化的开销,提高系统的响应速度和并发处理能力。

需要注意的是,线程池的预热只需在系统启动时执行一次即可,不需要在每个任务到来时都进行预热。预热的线程数应根据系统负载情况和性能需求进行合理设置,避免创建过多的线程导致资源浪费或创建过少的线程导致性能瓶颈。

案例

案例一:采用prestartCoreThread方法预热

public static void test1() {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(21060, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
    System.out.printf("线程池设置的核心线程数:%d,最大线程数:%d,当前线程数:%d",poolExecutor.getCorePoolSize(), poolExecutor.getMaximumPoolSize(),poolExecutor.getPoolSize()).println();
    poolExecutor.prestartCoreThread();
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.prestartCoreThread();
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.prestartCoreThread();
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.shutdown();
}

输出结果:

线程池设置的核心线程数:2,最大线程数:10,当前线程数:0
当前线程数:1
当前线程数:2
当前线程数:2

创建线程池的时候,设置核心线程数为2,最大线程数为10,调用prestartCoreThread预热前,线程池中线程数为0,连续两次调用prestartCoreThread,线程数分别加1,第三次调用prestartCoreThread后,发现线程数还是2,说明prestartCoreThread只能预热核心线程。

案例二:采用prestartAllCoreThreads方法预热

public static void test2() {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(21060, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
    System.out.printf("线程池设置的核心线程数:%d,最大线程数:%d,当前线程数:%d",poolExecutor.getCorePoolSize(), poolExecutor.getMaximumPoolSize(),poolExecutor.getPoolSize()).println();
    int count1 = poolExecutor.prestartAllCoreThreads();
    System.out.println("第一次调用prestartAllCoreThreads,返回线程数:" + count1);
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    int count2 = poolExecutor.prestartAllCoreThreads();
    System.out.println("第二次调用prestartAllCoreThreads,返回线程数:" + count2);
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.shutdown();
}

输出结果:

线程池设置的核心线程数:2,最大线程数:10,当前线程数:0
第一次调用prestartAllCoreThreads,返回线程数:2
当前线程数:2
第二次调用prestartAllCoreThreads,返回线程数:0
当前线程数:2

第一次使用prestartAllCoreThreads方法,启动线程数返回了2,也就是核心线程都启动了,第二次再调用的时候由于核心线程都已经启动了,所以返回0。

案例三:主动提交任务

public static void test3() {
    ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(31060, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
    System.out.printf("线程池设置的核心线程数:%d,最大线程数:%d,当前线程数:%d",poolExecutor.getCorePoolSize(), poolExecutor.getMaximumPoolSize(),poolExecutor.getPoolSize()).println();
    poolExecutor.submit(() -> {});
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.execute(() -> {});
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.execute(() -> {});
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.execute(() -> {});
    System.out.println("当前线程数:" + poolExecutor.getPoolSize());
    poolExecutor.shutdown();
}

输出结果:

线程池设置的核心线程数:3,最大线程数:10,当前线程数:0
当前线程数:1
当前线程数:2
当前线程数:3
当前线程数:3

核心线程数设置为3,最大线程数为10,通过submit和execute这两个方法提交逻辑为空的任务,也可以预热核心线程。主动提交任务这种方式预热线程其实跟调用线程池的prestartCoreThread效果一样,多提交几个任务跟多调用几次prestartCoreThread这个方法结果是一样的,预热的线程数最大都能达到corePoolSize。

问题

需要注意的是,线程池预热并不适用于所有场景。在一些长时间运行的系统中,线程池预热可能会导致资源浪费。因此,在具体应用中,需要根据系统的特点和需求来决定是否采用线程池预热策略。同时,还需要进行性能测试和评估,以确保线程池预热对系统性能的实际改进效果。

总结

关于线程池和预热经验之谈:

1.设置合适的核心线程数

核心线程数是线程池中保持活动状态的线程数量。在进行线程池预热时,需要根据系统的负载情况和任务的特性来设置合适的核心线程数。一般来说,核心线程数应该大于等于预热线程的数量,以确保预热线程能够得到充分利用。

2.控制预热时间和时机

预热时间和时机的选择对于线程池预热的效果至关重要。预热时间应该根据系统的响应速度和任务到来的频率来控制,预热时间需要满足确保线程池有足够的时间完成创建和初始化。预热时机应该合理选择,避免在系统负载高峰期进行预热,以免影响系统的正常运行。

3.预热线程数量的选择

预热线程的数量应该根据系统的负载情况和任务的特性来选择。过少的预热线程可能无法满足任务的并发需求,过多的预热线程可能会浪费系统资源。

4.动态调整线程池参数

在实际应用中,系统的负载和任务的特性可能会发生变化。为了适应这种变化,可以考虑动态调整线程池的参数,如核心线程数、最大线程数等,以保证线程池的性能和稳定性。

欢迎关注公众号,欢迎分享、点赞、在看

什么?竟然还有人不知道线程池预热?

原文始发于微信公众号(小新成长之路):什么?竟然还有人不知道线程池预热?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/238308.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!