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

计算密集型任务与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> {// 计算一至五十万数的总和(纯计算任务)@Overridepublic 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 resultlong 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 {@Overridepublic 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. java 线程池数量_java线程池及创建多少线程合适

    java线程池 1.以下是ThreadPoolExecutor参数完备构造方法: public ThreadPoolExecutor(int corePoolSize,int maximumPoolS ...

  2. JAVA线程池ThreadPoolExecutor创建以及各参数的详细说明

    最近把线程很多的东西都温故了一遍,发现还漏了个线程池,今天看了些线程池的文章,然后加入了自己的理解,总结如下 首先看下一个线程池的最简单的构造方法如下 * @param corePoolSize th ...

  3. java线程池_Java 并发编程 线程池源码实战

    作者 | 马启航 杏仁后端工程师.「我头发还多,你们呢?」 一.概述 笔者在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写 ...

  4. java线程池存在时间_Java线程池基础

    目录: 一.线程池概述 1.线程池类 目前线程池类一般有两个,一个来自于Spring,一个来自于JDK: 来自Spring的线程池:org.springframework.scheduling.con ...

  5. 谈谈java的线程池(创建、机制)

    目录 Executors创建线程池默认方法 自定义线程池 Executors创建线程池默认方法 newFixedThreadPool()方法,该方法返回一个固定数量的线程池,该方法的线程数始终不变,当 ...

  6. JUC多线程:线程池的创建及工作原理 和 Executor 框架

    一.什么是线程池: 线程池主要是为了解决 新任务执行时,应用程序为任务创建一个新线程 以及 任务执行完毕时,销毁线程所带来的开销.通过线程池,可以在项目初始化时就创建一个线程集合,然后在需要执行新任务 ...

  7. java(线程池的创建方式,和线程池的原理)

    1.为什么要使用线程池:   减少资源消耗,通过重复的使用已创建好的线程,避免了线程的频繁创建和销毁所造成的消耗 提高响应速度,当任务到达的时候,不需要再去创建,可以直接使用已经创建好的线程就能立即执 ...

  8. Java同步—线程池的创建和使用

    线程池 构建一个新的线程是有一定代价的,因为涉及到与操作系统的交互.如果程序中需要使用大量生命周期很短的线程,就应该使用线程池. 将Runnable对象交给线程池来执行,就会有一个线程调用run方法, ...

  9. JAVA线程池的创建

    /*** 创建不同类型的线程池 Executors* * @author */ public class ThreadPoolTest01 {public static void main(Strin ...

最新文章

  1. 分片哈希piecewise hashing
  2. 解读《信息系统灾难恢复规范》---转
  3. Windows7_x64下编译64位ffmpeg
  4. c语言编程输入a是输出为a_C ++编程基本输入,输出,数据类型,声明能力倾向问题和解答...
  5. 四十个非常实用的轻量级JavaScript库
  6. OSS客户端简单介绍和评测
  7. html css实验6,(实验六DivCSS网页布局.doc
  8. VS2019完全卸载教程
  9. 计算机cpu天体图,cpu性能天梯排行图 最新的2021年电脑cpu天梯图5月高清图
  10. Qt以文件资源管理器打开文件夹
  11. 要么听我的,要么走开(摘自《代码之道》第8章)
  12. 阿里云ECS学习资源
  13. 基于stm32单片机的台历日历计时器万年历Proteus仿真(源码+仿真+全套资料)
  14. 网络相册(只能本地)
  15. 微软让草稿几秒钟变App,还推出了个像Mac Mini的迷你台式机 | Build 2022
  16. 分布式限流的解决方案
  17. Linux内核模块it87出错
  18. 数据结构C语言狐狸抓兔子链表实现
  19. ExpressGridPack 21,PivotGrid 控件
  20. Python实现迪杰斯特拉算法

热门文章

  1. 人工智能技术应用就业前景和就业方向
  2. 【深度学习】实验1答案:Softmax实现手写数字识别
  3. canvas 模拟地球绕太阳转动 月球绕地球转动
  4. Leetcode面试题 10.01. 合并排序的数组(C语言)
  5. lua与C(一):C调用lua
  6. 在太平洋人寿保险公司工作好不好?
  7. springboot中使用freemarker生成word循环输出图片(二维码)
  8. 数学模型奶制品生产c语言编程,数学模型 奶制品的生产与销售.ppt
  9. 【博学谷学习记录】超强总结,用心分享|大数据之Hive
  10. 棉花1分钟时间周期的套利模型(有问题)