灵魂发问!线程池到底创建多少线程比较合理?

虽然线程池的模型被剖析的非常清晰,但是如何最高性能地使用线程池一直是一个令人纠结的问题,其中最主要的问题就是如何决定线程池的大小。这篇文章会以量化测试的方式分析:何种情况线程池应该使用多少线程数。

计算密集型任务与IO密集型任务

大多数刚接触线程池的人会认为有一个准确的值作为线程数能让线程池适用在程序的各个地方。然而大多数情况下并没有放之四海而皆准的值,很多时候我们要根据任务类型来决定线程池大小以达到最佳性能。

计算密集型任务以CPU计算为主,这个过程中会涉及到一些内存数据的存取(速度明显快于IO),执行任务时CPU处于忙碌状态。

IO密集型任务以IO为主,比如读写磁盘文件、读写数据库、网络请求等阻塞操作,执行IO操作时,CPU处于等待状态,等待过程中操作系统会把CPU时间片分给其他线程

计算密集型任务

下面写一个计算密集型任务的例子

public class ComputeThreadPoolTest {

    final static ThreadPoolExecutor computeExecutor;

    final static List<Callable<Long>> computeTasks;

    final static int task_count = 5000;

    static {
        computeExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1);

        // 创建5000个计算任务
        computeTasks = new ArrayList<>(task_count);
        for (int i = 0; i < task_count; i++) {
            computeTasks.add(new ComputeTask());
        }
    }

    static class ComputeTask implements Callable<Long> {
        // 计算一至五十万数的总和(纯计算任务)
        @Override
        public Long call() {
            long sum = 0;
            for (long i = 0; i < 50_0000; i++) {
                sum += i;
            }
            return sum;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 我电脑是四核处理器
        int processorsCount = Runtime.getRuntime().availableProcessors();
        // 逐一增加线程池的线程数
        for (int i = 1; i <=  processorsCount * 5; i++) {
            computeExecutor.setCorePoolSize(i);
            computeExecutor.setMaximumPoolSize(i);
            //直接创建所有核心线程并启动。
            computeExecutor.prestartAllCoreThreads();
            System.out.print(i);
            computeExecutor.invokeAll(computeTasks); // 预热所有线程,调用该方法会阻塞等待结果返回哦
            System.out.print("\t");
            //开始测试,测试8次
            testExecutor(computeExecutor, computeTasks);
            System.out.println();
            // 一定要让cpu休息会儿,Windows桌面操作系统不会让应用长时间霸占CPU
            // 否则Windows回收应用程序的CPU核心数将会导致测试结果不准确
            TimeUnit.SECONDS.sleep(5);// cpu rest
        }
        computeExecutor.shutdown();
    }

    private static <T> void testExecutor(ExecutorService executor, List<Callable<T>> tasks)
        throws InterruptedException {
        for (int i = 0; i < 8; i++) {
            long start = System.currentTimeMillis();
            executor.invokeAll(tasks); // ignore result
            long end = System.currentTimeMillis();
            System.out.print(end - start); // 记录时间间隔
            System.out.print("\t");
            TimeUnit.SECONDS.sleep(1); // cpu rest
        }
    }
}

将程序生成的数据粘贴到excel中,并对数据进行均值统计

注意如果相同的线程数两次执行的时间相差比较大,说明测试的结果不准确。

在这里插入图片描述

由于我笔记本的CPU有四个处理器,所以会发现当线程数达到4之后,5000个任务的执行时间并没有变得更少,基本上是在600毫秒左右徘徊。

因为计算机只有四个处理器可以使用,当创建更多线程的时候,这些线程是得不到CPU的执行的。

所以对于计算密集型任务,应该将线程数设置为CPU的处理个数,可以使用Runtime.availableProcessors方法获取可用处理器的个数。

《并发编程实战》一书中对于计算密集型任务建议线程池大小设为cpu核数+1,原因是当计算密集型线程偶尔由于页缺失故障或其他原因而暂停时,这个“额外的”线程也能确保这段时间内的CPU始终周期不会被浪费。

对于计算密集型任务,不要创建过多的线程,由于线程有执行栈等内存消耗,创建过多的线程不会加快计算速度,反而会消耗更多的内存空间;另一方面线程过多,频繁切换线程上下文也会影响线程池的性能

每个程序员都应该知道的延迟数

IO操作包括读写磁盘文件、读写数据库、网络请求等阻塞操作,执行这些操作,线程将处于等待状为了能更准确的模拟IO操作的阻塞,我觉得有必要将列举的延迟数整理出来。

参考网站:https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html

在这里插入图片描述

IO密集型任务

这里用sleep方式模拟IO阻塞:
public class IOThreadPoolTest {

    // 使用无限线程数的CacheThreadPool线程池
    static ThreadPoolExecutor cachedThreadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool();

    static List<Callable<Object>> tasks;

    // 仍然是5000个任务
    static int taskNum = 5000;

    static {
        tasks = new ArrayList<>(taskNum);
        for (int i = 0; i < taskNum; i++) {
            tasks.add(Executors.callable(new IOTask()));
        }
    }

    static class IOTask implements Runnable {

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        cachedThreadPool.invokeAll(tasks);// 同样的预热线程
        testExecutor(cachedThreadPool, tasks);
        // 看看执行过程中创建了多少个线程
        int largestPoolSize = cachedThreadPool.getLargestPoolSize();
        System.out.println("largestPoolSize:" + largestPoolSize);
        cachedThreadPool.shutdown();
    }

    private static void testExecutor(ExecutorService executor, List<Callable<Object>> tasks)
        throws InterruptedException {
        long start = System.currentTimeMillis();
        executor.invokeAll(tasks);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

}

这里使用无限制的CachedThreadPool线程池,也就是说这里的5000个任务会被5000个线程同时处理,由于所有的线程都只是阻塞而不消耗CPU资源,所以5000个任务在不到2秒的时间内就执行完了。

很明显使用CachedThreadPool能有效提高IO密集型任务的吞吐量,而且由于CachedThreadPool中的线程会在空闲60秒自动回收,所以不会消耗过多的资源。

但是打开任务管理器你会发现执行任务的同时内存会飙升到接近400M,因为每个线程都消耗了一部分内存,在5000个线程创建之后,内存消耗达到了峰值。
在这里插入图片描述

所以使用CacheThreadPool的时候应该避免提交大量长时间阻塞的任务,以防止内存溢出;另一种替代方案是,使用固定大小的线程池,并给一个较大的线程数(不会内存溢出),同时为了在空闲时节省内存资源,调用allowCoreThreadTimeOut允许核心线程超时。

线程执行栈的大小可以通过-Xsssize或-XX:ThreadStackSize参数调整

混合型任务

大多数任务并不是单一的计算型或IO型,而是IO伴随计算两者混合执行的任务——即使简单的Http请求也会有请求的构造过程。

混合型任务要根据任务等待阻塞时间与CPU计算时间的比重来决定线程数量:

比如一个任务包含一次数据库读写(0.1ms),并在内存中对读取的数据进行分组过滤等操作(5μs),那么线程数应该为80左右。

线程数与阻塞比例的关系图大致如下:
在这里插入图片描述

当阻塞比例为0,也就是纯计算任务,线程数等于核心数(这里是4);阻塞比例越大,线程池的线程数应该更多。

通常我们可以按此公式算出最佳核心线程数:cpu核数✖️(1+阻塞比例) ✖️ 70% ,系统中不止一个线程池,所以实际配置线程数应该将目标CPU利用率计算进去,也就是70%。

阻塞比例 = IO耗时 / Cpu耗时 , 我们平常可以用工具apm来统计这个比例

6. 总结

线程池的大小取决于任务的类型以及系统的特性,避免“过大”和“过小”两种极端。线程池过大,大量的线程将在相对更少的CPU和有限的内存资源上竞争,这不仅影响并发性能,还会因过高的内存消耗导致OOM;线程池过小,将导致处理器得不到充分利用,降低吞吐率。

要想正确的设置线程池大小,需要了解部署的系统中有多少个CPU,多大的内存,提交的任务是计算密集型、IO密集型还是两者兼有。

虽然线程池和JDBC连接池的目的都是对稀缺资源的重复利用,但通常一个应用只需要一个JDBC连接池,而线程池通常不止一个。如果一个系统要执行不同类型的任务,并且它们的行为差异较大,那么应该考虑使用多个线程池,使每个线程池可以根据各自的任务类型以及工作负载来调整。

清山绿水始于尘,博学多识贵于勤。
我有酒,你有故事吗?
欢迎一起谈天说地,聊Java。 回复「vip课程」,获取一套价值19820 元的 java vip课程
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目:思考与结论:人类成长的两个阶段 在我们的成长过程中,思考和结论是两个至关重要的过程。 小时候,我们对于世界充满好奇心,我们对周围的环境和事物充满探索欲望。我们喜欢提出各种问题,并寻找答案。我们天真地认为一切都有解决方法,只要我们能够找到答案。 随着我们的成长和经验积累,我们渐渐地变得注重结论和结果。在学校里,我们被要求完成各种任务,其目的就是为了要求我们得出正确的结论。我们被教育要追求“唯一正确”的答案,而不是多元化的可能性。我们开始失去我们的好奇心,变得麻木和钝化。 这种转变是自然而然的,由于经验和成熟度的增加,我们的思维变得更加成果导向。我们不再停留在探索的阶段,而是注重寻找答案。在工作和生活中,我们需要面对各种任务和挑战,我们需要得出结论并采取行动。 在这个进程中,我们丧失了思考的能力。我们渐渐地忘记了发问的重要性,忘记了我们曾经的好奇心和探索欲望。有些人甚至认为问问题和思考是浪费时间和资源。此时,我们面临的问题是,我们是否失去了重要的能力? 我们应该寻找一种平衡,既关注思考,也注重结论。好的思考瞄准问题的本质,我们应该寻找事物的更深层次,尝试寻找多种可能性。事实上,不同的人对同一个问题可能有不同的回答,我们应该尊重这样的特点。 在追求结论的过程中,我们应该审慎地选择正确的道路。我们应该采用科学的方法去。“唯一正确”并不一定是最好的答案,我们应该为探索真理而不是为了得到结果而去找答案。 综上所述,思考和结论是人类成长的两个阶段,我们应该在其中找到平衡点。在寻找答案和得出结论的同时,我们也应该保持好奇心和探索欲望,重视思考和探索。这样,我们可以发现真实的答案,并在成长中越来越成熟。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值