在 Java 中,创建线程会产生显著的成本。创建线程消耗时间,增加请求处理的延迟,并且涉及 JVM 和操作系统的大量工作。为了减轻这些开销,需要使用线程池。本文将深入探讨确定理想线程池大小的技巧。
性能上,创建和销毁线程在Java中代价较高,而线程池通过重用线程减少了这种开销。在可伸缩性方面,线程池能根据应用程序需求进行扩展,如重负载时处理更多任务。此外,线程池有助于资源管理,通过限制活动线程数来防止内存不足,从而确保应用程序稳定运行。
理解系统的局限性(包括硬件和外部依赖关系)对于调整线程池的大小至关重要,在本节进行举例说明,假设正在开发一个处理 HTTP 请求的 Web 应用,每个请求可能涉及处理来自数据库的数据和对外部第三方服务的调用。目标是确定有效处理这些请求的最佳线程池大小,考虑因素如下包括数据库连接池,服务的吞吐量以及CPU核数。
假设使用HikariCP 连接池来管理数据库连接,已将其配置为允许最多100个连接。如果创建的线程超过可用连接的数量,额外的线程最终将等待可用连接,从而导致资源争用和潜在的性能问题。
下面是一个配置 HikariCP 数据库连接池的示例:
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/mydb");
config.setUsername("username");
config.setPassword("password");
config.setMaximumPoolSize(100); // Set the maximum number of connections
HikariDataSource dataSource = new HikariDataSource(config);
// Use the dataSource to get database connections and perform queries.
}
}
与应用程序交互的外部服务是一个限制,同时处理的请求请求数有限,比如一次10个请求。更多的并发请求可能会使服务不堪重负,并导致性能下降或出现错误。
确定服务器上可用的 CPU 内核数量对于优化线程池大小至关重要。
int numOfCores = Runtime.getRuntime().availableProcessors();
每个核心可以并发执行一个线程,超过 CPU 核数的线程会导致过多的上下文切换,从而降低性能。
就应用场景的任务类型而言,一般可以分为CPU密集型任务和 I/O 密集型任务,不同类型的类型的任务有着不同的优化方法。
CPU 密集型任务是那些需要大量处理能力的任务,比如执行复杂的计算或运行仿真。这些任务通常受到 CPU 速度的限制,而不是 I/O 设备的速度,例如:
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};
int numThreads = Runtime.getRuntime().availableProcessors(); // Get the number of CPU cores
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) {
// Simulate a time-consuming calculation (e.g., database query, complex computation)
try {
Thread.sleep(1000); // Simulate a 1-second delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return number * number;
}
}
I/O 密集型任务是那些与存储设备(例如,读/写文件)、网络套接字(例如,进行 API 调用)或用户输入(例如,GUI中的用户交互)交互的任务。例如:
面对I/O密集型任务,一般的优化方法如下:
对于受 CPU 限制的任务,希望最大限度地提高 CPU 利用率,同时又不要让太多线程压垮系统,这可能导致过多的上下文切换。一个常见的经验法则是使用可用的 CPU 核数。
假设开发一个视频处理应用程序。视频编码是一项受 CPU 限制的任务,需要应用复杂的算法来压/解缩视频文件。另,假设有一个多核的 CPU 可用。
确定 CPU 绑定任务的线程数,在Java中使用 Runtime.getRuntime().availableProcessors() 以确定可用的 CPU 核心的数量,这里假设有8个核。然后,创建一个大小接近或略小于可用CPU 核数的线程池。在下面的示例中,可以选择6或7个线程,为其他任务和系统进程保留一些 CPU 容量。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VideoEncodingApp {
public static void main(String[] args) {
int availableCores = Runtime.getRuntime().availableProcessors();
int numberOfThreads = Math.max(availableCores - 1, 1); // Adjust as needed
ExecutorService threadPool = Executors.newFixedThreadPool(numberOfThreads);
// Submit video encoding tasks to the thread pool.
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
encodeVideo(); // Simulated video encoding task
});
}
threadPool.shutdown();
}
private static void encodeVideo() {
// Simulate video encoding (CPU-bound) task.
// Complex calculations and compression algorithms here.
}
}
对于 I/O 密集型的任务,最佳线程数通常由 I/O 操作的性质和预期的延迟决定。我们希望拥有足够的线程来保持 I/O 设备忙碌,而不会使它们超载。理想的数目可能不一定等于 CPU 核数。
假设构建一个网页爬虫,可以下载网页并提取信息。这涉及到发出 HTTP 请求,由于网络延迟,HTTP 请求是 I/O 绑定的任务。
确定 I/O 绑定任务的线程数,需估计预期的 I/O 延迟,这取决于网络或存储。例如,如果每个 HTTP 请求需要大约500毫秒才能完成,那么可能需要适应 I/O 操作中的一些重叠。然后,创建一个具有平衡并行性和预期 I/O 延迟的大小的线程池。我们不一定需要每个任务一个线程; 相反,可以使用一个更小的池来有效地管理 I/O 绑定的任务。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WebPageCrawler {
public static void main(String[] args) {
int expectedIOLatency = 500; // Estimated I/O latency in milliseconds
int numberOfThreads = 4; // Adjust based on your expected latency and system capabilities
ExecutorService threadPool = Executors.newFixedThreadPool(numberOfThreads);
// List of URLs to crawl.
String[] urlsToCrawl = {
"https://example.com",
"https://google.com",
"https://github.com",
// Add more URLs here
};
for (String url : urlsToCrawl) {
threadPool.execute(() -> {
crawlWebPage(url, expectedIOLatency);
});
}
threadPool.shutdown();
}
private static void crawlWebPage(String url, int expectedIOLatency) {
// Simulate web page crawling (I/O-bound) task.
// Perform HTTP request and process the page content.
try {
Thread.sleep(expectedIOLatency); // Simulating I/O latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
确定线程池大小的公式如下:
线程数 = 可用内核数 * 目标 CPU 利用率 * (1 + 等待时间/服务时间)
其中:
假设有一个具有4核 CPU 的服务器,并希望应用程序使用50% 的可用 CPU 资源。我们的应用程序有两类任务: I/O 密集型任务和 CPU 密集型任务。其中,I/O 密集型任务的阻塞系数为0.5,这意味着它们要花费50% 的时间等待 I/O 操作完成。
I/O密集型任务的线程数 = 4个核心 * 0.5 * (1 + 0.5) = 3个线程
CPU 密集型任务的阻塞系数为0.1,这意味着它们要花费10% 的时间等待 I/O 操作完成。
CPU 密集型任务线程数 = 4个核心 * 0.5 * (1 + 0.1) = 2.2个线程
在这一示例中,我们创建了两个线程池,一个用于 I/O 密集型任务,另一个用于 CPU 密集型任务。I/O 密集型线程池将有3个线程,CPU 密集型线程池将有2个线程。
调优的线程池大小可以从系统中得到最佳性能,并优雅地分配工作负载,虽为雕虫小技,但积小胜为大胜,对系统性能而言,尤其如此。