【Java】灵魂发问!线程池到底创建多少线程比较合理?
灵魂发问!线程池到底创建多少线程比较合理?
java锦囊发布于 26 分钟前
虽然线程池的模型被剖析的非常清晰,但是如何最高性能地使用线程池一直是一个令人纠结的问题,其中最主要的问题就是如何决定线程池的大小。这篇文章会以量化测试的方式分析:何种情况线程池应该使用多少线程数。
计算密集型任务与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操作包括读写磁盘文件、读写数据库、网络请求等阻塞操作,执行这些操作,线程将处于等待状为了能更准确的模拟IO操作的阻塞,我觉得有必要将列举的延迟数整理出来。
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允许核心线程超时。
混合型任务
大多数任务并不是单一的计算型或IO型,而是IO伴随计算两者混合执行的任务——即使简单的Http请求也会有请求的构造过程。
混合型任务要根据任务等待阻塞时间与CPU计算时间的比重来决定线程数量:
比如一个任务包含一次数据库读写(0.1ms),并在内存中对读取的数据进行分组过滤等操作(5μs),那么线程数应该为80左右。
线程数与阻塞比例的关系图大致如下:
当阻塞比例为0,也就是纯计算任务,线程数等于核心数(这里是4);阻塞比例越大,线程池的线程数应该更多。
通常我们可以按此公式算出最佳核心线程数:cpu核数✖️(1+阻塞比例) ✖️ 70% ,系统中不止一个线程池,所以实际配置线程数应该将目标CPU利用率计算进去,也就是70%。
6. 总结
线程池的大小取决于任务的类型以及系统的特性,避免“过大”和“过小”两种极端。线程池过大,大量的线程将在相对更少的CPU和有限的内存资源上竞争,这不仅影响并发性能,还会因过高的内存消耗导致OOM;线程池过小,将导致处理器得不到充分利用,降低吞吐率。
要想正确的设置线程池大小,需要了解部署的系统中有多少个CPU,多大的内存,提交的任务是计算密集型、IO密集型还是两者兼有。
虽然线程池和JDBC连接池的目的都是对稀缺资源的重复利用,但通常一个应用只需要一个JDBC连接池,而线程池通常不止一个。如果一个系统要执行不同类型的任务,并且它们的行为差异较大,那么应该考虑使用多个线程池,使每个线程池可以根据各自的任务类型以及工作负载来调整。
清山绿水始于尘,博学多识贵于勤。
我有酒,你有故事吗?
欢迎一起谈天说地,聊Java。 回复「vip课程」,获取一套价值19820 元的 java vip课程
java线程池" title="java线程池">java线程池
https://blog.hufeifei.cn/2018/07/29/Java/
阅读 19发布于 26 分钟前
java锦囊
关注公众号【java锦囊】,回复 「vip课程」,获取一套价值19280元的java vip 课程
1 声望
0 粉丝
java锦囊
关注公众号【java锦囊】,回复 「vip课程」,获取一套价值19280元的java vip 课程
1 声望
0 粉丝
宣传栏
虽然线程池的模型被剖析的非常清晰,但是如何最高性能地使用线程池一直是一个令人纠结的问题,其中最主要的问题就是如何决定线程池的大小。这篇文章会以量化测试的方式分析:何种情况线程池应该使用多少线程数。
计算密集型任务与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操作包括读写磁盘文件、读写数据库、网络请求等阻塞操作,执行这些操作,线程将处于等待状为了能更准确的模拟IO操作的阻塞,我觉得有必要将列举的延迟数整理出来。
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允许核心线程超时。
混合型任务
大多数任务并不是单一的计算型或IO型,而是IO伴随计算两者混合执行的任务——即使简单的Http请求也会有请求的构造过程。
混合型任务要根据任务等待阻塞时间与CPU计算时间的比重来决定线程数量:
比如一个任务包含一次数据库读写(0.1ms),并在内存中对读取的数据进行分组过滤等操作(5μs),那么线程数应该为80左右。
线程数与阻塞比例的关系图大致如下:
当阻塞比例为0,也就是纯计算任务,线程数等于核心数(这里是4);阻塞比例越大,线程池的线程数应该更多。
通常我们可以按此公式算出最佳核心线程数:cpu核数✖️(1+阻塞比例) ✖️ 70% ,系统中不止一个线程池,所以实际配置线程数应该将目标CPU利用率计算进去,也就是70%。
6. 总结
线程池的大小取决于任务的类型以及系统的特性,避免“过大”和“过小”两种极端。线程池过大,大量的线程将在相对更少的CPU和有限的内存资源上竞争,这不仅影响并发性能,还会因过高的内存消耗导致OOM;线程池过小,将导致处理器得不到充分利用,降低吞吐率。
要想正确的设置线程池大小,需要了解部署的系统中有多少个CPU,多大的内存,提交的任务是计算密集型、IO密集型还是两者兼有。
虽然线程池和JDBC连接池的目的都是对稀缺资源的重复利用,但通常一个应用只需要一个JDBC连接池,而线程池通常不止一个。如果一个系统要执行不同类型的任务,并且它们的行为差异较大,那么应该考虑使用多个线程池,使每个线程池可以根据各自的任务类型以及工作负载来调整。
清山绿水始于尘,博学多识贵于勤。
我有酒,你有故事吗?
欢迎一起谈天说地,聊Java。 回复「vip课程」,获取一套价值19820 元的 java vip课程
以上是 【Java】灵魂发问!线程池到底创建多少线程比较合理? 的全部内容, 来源链接: utcz.com/a/105578.html
得票时间