关于java:灵魂发问线程池到底创建多少线程比较合理

26次阅读

共计 4647 个字符,预计需要花费 12 分钟才能阅读完成。

尽管线程池的模型被分析的十分清晰,然而如何最高性能地应用线程池始终是一个令人纠结的问题,其中最次要的问题就是如何决定线程池的大小。这篇文章会以量化测试的形式剖析:何种状况线程池应该应用多少线程数。

计算密集型工作与 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 办法获取可用处理器的个数。

《并发编程实战》一书中对于 IO 密集型工作倡议线程池大小设为cpu 核数 +1,起因是当计算密集型线程偶然因为页缺失故障或其余起因而暂停时,这个“额定的”线程也能确保这段时间内的 CPU 始终周期不会被节约。

对于计算密集型工作,不要创立过多的线程,因为线程有执行栈等内存耗费,创立过多的线程不会放慢计算速度,反而会耗费更多的内存空间;另一方面线程过多,频繁切换线程上下文也会影响线程池的性能

每个程序员都应该晓得的提早数

IO 操作包含读写磁盘文件、读写数据库、网络申请等阻塞操作,执行这些操作,线程将处于期待状为了能更精确的模仿 IO 操作的阻塞,我感觉有必要将列举的提早数整理出来。

参考网站:https://people.eecs.berkeley….

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 连接池,而线程池通常不止一个。如果一个零碎要执行不同类型的工作,并且它们的行为差别较大,那么应该思考应用多个线程池,使每个线程池能够依据各自的工作类型以及工作负载来调整。

本文整顿自:https://blog.hufeifei.cn/2018…

清山绿水始于尘,博学多识贵于勤。
我有酒,你有故事吗?
欢送一起谈天说地,聊 Java。回复「vip 课程」,获取一套价值 19820 元的 java vip 课程

正文完
 0