Java线程的创建会花费不少时间,还得让JVM和操作系统忙活,可累人了。所以,为了减少这些额外的开销,就出现了线程池的技术,这可真是帮了大忙。
那么,我们接下来要探讨一下,怎么确定理想的线程池大小,才能让系统表现最好,还能轻松应对突然增加的工作量。不过,咱们也得记住,就是用了线程池,线程管理本身也可能成为瓶颈哦。
线程池的好处
- 性能:创建和销毁线程很费劲,特别是在Java中。线程池可以创建一些可以重复用的线程,用来帮助减少这种开销。
- 能扩展:线程池可以根据应用程序的需求来扩展。比如,要是负载很重的话,就可以把线程池扩展一下,处理更多的任务。
- 能合理管理资源:线程池可以帮助管理使用的资源。比方说,线程池可以限制在任何给定时间能活动的线程数量,这样就能防止应用程序用光内存。
调整线程池大小:了解系统和资源的限制
在调整线程池大小时,了解系统的限制(包括硬件和外部依赖性)特别重要。咱们用一个例子来详细说明一下这个概念:
场景:
假设你正在开发一个处理HTTP请求的Web应用程序。每个请求可能涉及到处理数据库中的数据以及调用外部第三方服务。你的目标是确定能有效地处理这些请求的最佳线程池大小。
需要考虑的因素:
假设你使用HikariCP等连接池来管理数据库连接,并且已将其配置为最多允许100个连接。如果你创建的线程比这个连接数还多,那些额外的线程就得等着有连接可用,这样就会导致资源争用,可能会引发性能问题。
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class DatabaseConnectionExample {
public static void main(String[] args) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("root");
config.setMaximumPoolSize(100); // 设置最大连接数
HikariDataSource dataSource = new HikariDataSource(config);
// 使用dataSource获取数据库连接并执行查询。
}
}
外部服务吞吐量:除了处理外部服务请求的能力之外,还要考虑服务之间的交互和数据传输速度。
比如,如果两个服务之间需要频繁地传输大量数据,那么就需要看看网络带宽和数据传输速度能不能满足要求,要不然数据传输速度慢可能会导致服务响应时间变长,进而影响整个程序的性能。
CPU 核心:要优化线程池大小的时候,知道服务器上有多少 CPU 核心真的很重要。
比如,如果服务器有4个CPU核心,那么线程池大小设置为4或5可能是一个合适的选择,这样可以充分利用服务器的资源,同时避免过多的线程去争抢CPU资源。但是,如果服务器只有一个CPU核心,那么线程池的大小就不能设置得太大,否则可能会导致线程之间的竞争过于激烈,反而降低应用程序的性能。所以,设置线程池大小的时候,要根据服务器的硬件配置和应用程序的实际情况综合考虑。
int numOfCores = Runtime.getRuntime().availableProcessors();
每个CPU核心可以同时执行一个线程。如果超过线程的 CPU 核心数量可能会导致频繁的上下文切换,从而大大降低性能。
介绍CPU密集型和IO密集型任务
CPU密集型任务和IO密集型任务是两种不同类型的计算任务,它们的性质和资源需求有所不同。
- CPU密集型任务:
- CPU密集型任务是指需要大量计算能力的任务,例如数学运算、图形处理、数据分析、编码和解码视频等。
- 这些任务通常会占用大量的CPU资源,因为它们需要处理大量的计算操作,而与硬盘或网络通信的频率相对较低。
- 通常情况下,提高CPU性能(如提高CPU时钟频率或使用多核CPU)可以改善CPU密集型任务的执行性能。
多线程和并行性:并行处理是一种技术,用于将较大的任务划分为较小的子任务,并将这些子任务分布在多个 CPU 核心或处理器上,以利用并发执行并提高整体性能。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ParallelSquareCalculator {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 获取cpu核心数
int numThreads = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
for (int number : numbers) {
executorService.submit(() -> {
int square = calculateSquare(number);
System.out.println("Square of " + number + " is " + square);
});
}
executorService.shutdown();
try {
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private static int calculateSquare(int number) {
// 模拟一次耗时的计算
try {
Thread.sleep(1000); // Simulate a 1-second delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return number * number;
}
}
- IO密集型任务:
- IO密集型任务是指需要大量的输入/输出操作(例如读取文件、数据库查询、网络通信)的任务。
- 这些任务通常不需要大量的计算能力,但需要等待IO操作完成,因此它们可能会占用大量的时间。
- 对于IO密集型任务,提高CPU性能通常不会显著提高性能,因为瓶颈通常是在IO操作上。
- 优化IO密集型任务的关键在于减少等待时间,例如使用异步IO、多线程或多进程等技术。
优化逻辑:
- 缓存:将频繁访问的数据缓存在内存中,以减少重复 I/O 操作的需要。
- 负载平衡:将 I/O 密集型任务分配到多个线程或进程,以有效处理并发 I/O 操作。
- SSD 的使用:与传统硬盘驱动器 (HDD) 相比,固态驱动器 (SSD) 可以显着加快 I/O 操作速度。
- 使用高效的数据结构(例如哈希表和 B 树)来减少所需的 I/O 操作数量。
- 避免不必要的文件操作,例如多次打开和关闭文件。
要理解一个任务是CPU密集型还是IO密集型,可以考虑任务的特点:
- 如果任务的主要瓶颈是计算操作,它很可能是CPU密集型。
- 如果任务的主要瓶颈是等待外部资源(例如磁盘、网络)的可用性,它很可能是IO密集型。
在实际应用中,优化CPU密集型任务通常涉及提高CPU性能,而优化IO密集型任务通常涉及减少IO操作的延迟,以提高整体性能。
确定线程数
如果是CPU 密集型任务较多的线程数:
- 计算可用 CPU 核心数:在 Java 中用于Runtime.getRuntime().availableProcessors()确定可用 CPU 核心的数量。假设您有 8 个核心。
- 创建线程池:创建大小接近或略小于可用CPU核心数的线程池。在这种情况下,您可以选择 6 或 7 个线程,为其他任务和系统进程留下一些 CPU 容量。
如果是IO密集型:
对于 I/O 密集型任务,最佳线程数通常由 I/O 操作的性质和预期延迟决定,不一定等于CPU核心的数量。
确定 I/O 密集型任务的线程数:
- 分析 I/O 延迟:估计预期的 I/O 延迟,这取决于网络或存储。例如,如果每个 HTTP 请求大约需要 500 毫秒才能完成,您可能需要适应 I/O 操作中的一些重叠。
- 创建线程池:创建一个大小能够平衡并行性与预期 I/O 延迟的线程池。每个任务不一定需要一个线程;相反,您可以使用较小的池来有效管理 I/O 密集型任务。
计算线程池线程数公式
确定线程池大小的公式可以写成如下:
线程数 = 可用核心数 * 目标 CPU 利用率 * (1 + 等待时间 / 服务时间)
可用核心数:这是您的应用程序可用的CPU 核心数。需要注意的是,这与 CPU 的数量不同,因为每个 CPU 可能有多个核心。
目标 CPU 利用率:这是您希望应用程序使用的CPU 时间的百分比。如果您将目标 CPU 利用率设置得太高,您的应用程序可能会变得无响应。如果设置得太低,您的应用程序将无法充分利用可用的 CPU 资源。
等待时间:这是线程等待 I/O 操作完成所花费的时间。这可能包括等待网络响应、数据库查询或文件操作。
服务时间:这是线程执行计算所花费的时间量。
阻塞系数:这是等待时间与服务时间的比率。它衡量线程等待 I/O 操作完成所花费的时间相对于执行计算所花费的时间。
用法示例
假设您有一台具有 4 个 CPU 核心的服务器,并且您希望应用程序使用 50% 的可用 CPU 资源。
您的应用程序有两类任务:I/O 密集型任务和 CPU 密集型任务。
I/O 密集型任务的阻塞系数为 0.5,这意味着它们花费 50% 的时间等待 I/O 操作完成。
线程数 = 4 核 * 0.5 * (1 + 0.5) = 3 线程
CPU 密集型任务的阻塞系数为 0.1,这意味着它们花费 10% 的时间等待 I/O 操作完成。
线程数 = 4 核 * 0.5 * (1 + 0.1) = 2.2 线程
在此示例中,您将创建两个线程池,一个用于 I/O 密集型任务,另一个用于 CPU 密集型任务。I/O 密集型线程池将有 3 个线程,CPU 密集型线程池将有 2 个线程。
如果各位觉得老七的文章还不错的话,麻烦大家动动小手,
点赞、关注、转发走一波!!
有任何问题可以评论区留言或者私信我,我必将知无不言言无不尽!