去年看过一篇《ThreadPoolExecutor详解》大致讲了ThreadPoolExecutor内部的代码实现。

总结一下,主要有以下四点:

当有任务提交的时候,会创建核心线程去执行任务(即使有核心线程空闲仍会创建);
当核心线程数达到corePoolSize时,后续提交的都会进BlockingQueue中排队;
当BlockingQueue满了(offer失败),就会创建临时线程(临时线程空闲超过一定时间后,会被销毁);
当线程总数达到maximumPoolSize时,后续提交的任务都会被RejectedExecutionHandler拒绝。
prestartAllCoreThreads方法可以直接创建所有核心线程并启动。

BlockingQueue使用无限容量的阻塞队列(如LinkedBlockingQueue)时,不会创建临时线程(因为队列不会满),所以线程数保持corePoolSize。

BlockingQueue使用没有容量的同步队列(如SynchronousQueue)时,任务不会入队,而是直接创建临时线程去执行任务。

虽然线程池的模型被剖析的非常清晰,但是如何最高性能地使用线程池一直是一个令人纠结的问题,其中最主要的问题就是如何决定线程池的大小。

这篇文章会以量化测试的方式分析:何种情况线程池应该使用多少线程数。

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

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

IO密集型任务以IO为主,比如读写磁盘文件、读写数据库、网络请求等阻塞操作,执行IO操作时,CPU处于等待状态。

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

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); // warm up all thread
System.out.print("\t");
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密集型任务建议线程池大小设为Ncpu+1Ncpu+1,原因是当计算密集型线程偶尔由于页缺失故障或其他原因而暂停时,这个“额外的”线程也能确保这段时间内的CPU始终周期不会被浪费。

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

3. 每个程序员都应该知道的延迟数
IO操作包括读写磁盘文件、读写数据库、网络请求等阻塞操作,执行这些操作,线程将处于等待状态。

为了能更准确的模拟IO操作的阻塞,我觉得有必要将https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html中列举的延迟数整理出来。

事件 纳秒 微秒 毫秒 对照
一级缓存 0.5 - - -
二级缓存 7 - - 一级缓存时间14倍
互斥锁定/解锁 25.0 - - -
主存参考 100.0 - - 二级缓存20倍,一级缓存200倍
使用Zippy压缩1K字节 3,000.0 3 - -
通过1Gbps网络发送1K字节 10,000.0 10 - -
从SSD中随机读取4K 150,000.0 150 - 1GB/秒的读取速度的SSD硬盘
从内存中顺序读取1MB 250,000.0 250 - -
在同一数据中心局域网内往返 500,000.0 500 - -
从SSD顺序读取1MB 1,000,000.0 1000 1 1GB/秒SSD,4X 内存
磁盘搜寻 10,000,000.0 10000 10 20X 数据中心往返
从磁盘顺序读取1MB 20,000,000.0 20000 20 80X 内存,20X SSD
发送一个数据包
美国加州→荷兰→加州 150,000,000.0 150000 150 -
4. 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);// warm up all thread
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允许核心线程超时。

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

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

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

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

线程数与阻塞比例的关系图大致如下:

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

《Java并发编程实战》中最原始的公式是这样的:
>Nthreads=Ncpu∗Ucpu∗(1+WC)>
>Nthreads=Ncpu∗Ucpu∗(1+WC)>

NcpuNcpu代表CPU的个数,UcpuUcpu代表CPU利用率的期望值(0<Ucpu<10<Ucpu<1),WCWC仍然是等待时间与计算时间的比例。
我上面提供的公式相当于目标CPU利用率为100%。

通常系统中不止一个线程池,所以实际配置线程数应该将目标CPU利用率计算进去。

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

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

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

参考链接:

https://stackoverflow.com/questions/868568/what-do-the-terms-cpu-bound-and-i-o-bound-mean
https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html
https://en.wikipedia.org/wiki/Amdahl%27s_law
http://baddotrobot.com/blog/2013/06/01/optimum-number-of-threads/
《并发编程实战》:java concurrent in practice

ThreadPoolExecutor最佳实践--如何选择线程数的更多相关文章

  1. 分布式 PostgreSQL 集群(Citus),分布式表中的分布列选择最佳实践

    确定应用程序类型 在 Citus 集群上运行高效查询要求数据在机器之间正确分布.这因应用程序类型及其查询模式而异. 大致上有两种应用程序在 Citus 上运行良好.数据建模的第一步是确定哪些应用程序类 ...

  2. 经典的性能优化最佳实践 web性能权威指南 读书笔记

    web性能权威指南 page 203 经典的性能优化最佳实践 无论什么网络,也不管所用网络协议是什么版本,所有应用都应该致力于消除或减 少不必要的网络延迟,将需要传输的数据压缩至最少.这两条标准是经典 ...

  3. 验证Kubernetes YAML的最佳实践和策略

    本文来自Rancher Labs Kubernetes工作负载最常见的定义是YAML格式的文件.使用YAML所面临的挑战之一是,它相当难以表达manifest文件之间的约束或关系. 如果你想检查所有部 ...

  4. RDS最佳实践(一)—如何选择你的RDS

    在去年双11之前,为了帮助商家准备天猫双11的大促,让用户更好的使用RDS,把RDS的性能发挥到最佳,保障双11当天面对爆发性增加的压力,不会由于RDS的瓶颈导致系统出现问题,编写了 RDS的最佳实践 ...

  5. Java并发(八)计算线程池最佳线程数

    目录 一.理论分析 二.实际应用 为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务.并且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执行.在高并发的情况下采用线程池,可以有 ...

  6. atitit.标准时间格式 互相转换 秒数 最佳实践

    atitit.标准时间格式 互相转换 秒数 最佳实践 例如00:01:19 转换为秒数  79,,and互相转换 一个思路是使用div 60 mod...不过麻烦的... 更好的方法是使用stamp ...

  7. paip.提升效率--gui 的选择--swing最佳实践swt awt

    paip.提升效率--gui 的选择--swing最佳实践swt awt ////////////////弹出消息框. ////////////////myeclipse swing 开发最佳实践.. ...

  8. Jboss调优——最佳线程数

     在设置jboss的参数中,maxThreads(最大线程数)和acceptCount(最大等待线程数)是两个非常重要的指标,直接影响到程序的QPS.本文讲解jboss连接的运行原理,以及如何设置这两 ...

  9. Tomcat设置最佳线程数总结

    最佳线程数: 性能压测的情况下,起初随着用户数的增加,QPS会上升,当到了一定的阀值之后,用户数量增加QPS并不会增加,或者增加不明显,同时请求的响应时间却大幅增加.这个阀值我们认为是最佳线程数. 为 ...

随机推荐

  1. input 的radio checkbox 和 select 相关操作

    1  select 获取和设置值,以及onchange事件 1下拉框option没有checked事件 可通过select 的 onchange事件进行监控,以获取其值 <select name ...

  2. QQ项目(续)

    1.项目查找好友的原理 sql:select * from qquser where account in(select friendAccount from friend where userAcc ...

  3. scrapy selenium 登陆zhihu

    # -*- coding: utf-8 -*- # 导入依赖包 import scrapy from selenium import webdriver import time import json ...

  4. Docker学习笔记之搭建 Java Web 项目运行环境

    0x00 概述 Java Web 泛指以 Java 程序为基础向外提供 Web 服务的技术及相关工具,狭义上来说,我们也可以说 Java Web 是由 Servlet 程序提供的 Web 服务. 对我 ...

  5. IOS 苹果手机fiddler抓包时出现了tunnel to 443 解决方案,亲测有效

    先上一张捉取成功图[版本需4.0以上,并非所有https数据可抓取,具体原因未知] 1.先对Fiddler进行设置[打开Fiddler ——> Options .然后打开的对话框中,选择HTTP ...

  6. tomcat报java.lang.VerifyError错误

    google结果: 针对“java.lang.VerifyError”的错误原因,主要是因为jar包的版本问题导致,可能是因为部署环境存在2套以上版本冲突的JDBC驱动程序部署在应用服务器不同的lib ...

  7. 通过shell查找访问日志中访问量最大的ip

    日志格式: /Sep/::: +] /Sep/::: +] /Sep/::: +] - /Sep/::: +] - /Sep/::: +] /Sep/::: +] - /Sep/::: +] /Sep ...

  8. 基础_cifar10_model

    今天进一步在cifar10数据集上解决几个问题: 1.比较一下序贯和model,为什么要分成两块: 2.同样的条件下,我去比较一下序贯和model.这个例子作为今天的晚间运行. 1.比较一下序贯和mo ...

  9. 20155201 网络攻防技术 实验五 MSF基础应用

    20155201 网络攻防技术 实验五 MSF基础应用 一.实践内容 一个主动攻击实践,如ms08_067 一个针对浏览器的攻击,如ms11_050 一个针对客户端的攻击,如Adobe 成功应用任何一 ...

  10. JS、JAVA刷题和C刷题的一个很重要的区别

    就是最近在做树方面的题时,发现JS和JAVA刷题和C刷题的一个很重要的区别就是传入null的区别 当遍历的时候,C传参数时可以传进去null的指针,因为递归进去,出来时,指针还是指着那个地方 但是JS ...