如何正确判断Java线程池的大小

开发 前端
线程池(ThreadPool)是由执行器服务(executor service)管理的工作线程池。其理念是重用现有线程,而不是为每个任务创建新线程。

在Java中,线程创建会产生显著的开销。创建线程会消耗时间,增加请求处理的延迟,并且涉及JVM和操作系统的相当多工作。为减轻这些开销,线程池应运而生。

线程池(ThreadPool)是由执行器服务(executor service)管理的工作线程池。其理念是重用现有线程,而不是为每个任务创建新线程。这可以通过减少线程创建的开销来显著提高应用程序的性能。Java的ExecutorService和ThreadPoolExecutor类提供了管理线程池的框架。

关键点

  • 线程重用:线程池的线程可用于多个任务的重用。
  • 任务排队:任务被提交到池中,池中的线程会提取并执行这些任务。
  • 资源管理:可配置线程池大小、任务队列大小和其他参数,以高效管理资源。

1. 使用线性池的原因

  • 性能:线程的创建和销毁成本较高,尤其在Java中。创建一个可供多任务重用的线程池可减少这种开销。
  • 可扩展性:线程池可根据应用程序的需要进行扩展。例如,在负载较重时,可扩展线程池以处理额外的任务。
  • 资源管理:线程池可帮助管理线程使用资源。例如,线程池可限制同时活动的线程数量,防止应用程序内存不足。

2. 确定线程池大小:理解系统和资源限制

了解包括硬件和外部依赖关系的系统限制,对于确定线程池大小至关重要。下面通过一个例子来详细说明这一概念。

假设在开发一个处理传入HTTP请求的Web应用程序,每个请求可能涉及数据库处理数据和调用外部第三方服务。目标是确定处理这些请求的最佳线程池大小。

此情况下需考虑因素包含数据库连接池与外部服务吞吐量两方面。

  • 数据库连接池:假设使用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); // 设置最大连接数

        HikariDataSource dataSource = new HikariDataSource(config);

        // 使用 dataSource 获取数据库连接并执行查询。
    }
}
  • 外部服务吞吐量:此应用程序交互的外部服务有一个限制。它一次只能处理少量请求,例如10个请求。并发更多请求可能会使服务不堪重负,导致性能下降或出现错误。CPU核心确定服务器上可用的CPU核心数量对于优化线程池大小至关重要。
int numOfCores = Runtime.getRuntime().availableProcessors();

每个核心可以同时执行一个线程。超出CPU核心数量的线程会导致过度的上下文切换,从而降低性能。

3.CPU密集型任务与I/O密集型任务

CPU密集型任务

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(); // 获取 CPU 核心数量
        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); // 模拟 1 秒延迟
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        return number * number;
    }
}

I/O密集型任务

I/O密集型任务是那些与存储设备(例如,读写文件)、网络套接字(例如API调用)或用户输入进行交互(例如图形用户界面的交互)的任务,下面是一些典型的I/O密集型任务。

  • 读取或写入大文件到磁盘(例如,保存视频文件、加载数据库)
  • 通过网络下载或上传文件(例如,浏览网页、观看流媒体视频)
  • 发送和接收电子邮件
  • 运行Web服务器或其他网络服务
  • 执行数据库查询
  • Web服务器处理传入请求

优化I/O密集型任务的方式

  • 在内存中缓存频繁访问的数据,以减少重复I/O操作的需要。
  • 负载均衡,将I/O密集型任务分配到多个线程或进程中,以高效处理并发I/O操作。
  • 使用SSD,固态硬盘(SSDs)相较于传统硬盘(HDDs)可以显著加快I/O操作。
  • 使用高效数据结构,如哈希表和B树,以减少所需的I/O操作次数。
  • 避免不必要的文件操作,例如多次打开和关闭文件。

4.在两种任务中确定线程数

确定CPU密集型任务的线程数量

对于CPU密集型任务,要最大化CPU利用率,而不让系统因线程过多而超负荷,防止过度的上下文切换。一个常见方法是使用可用CPU核心数量。假设需开发一个视频处理应用程序。视频编码是一项CPU密集型任务,需要应用复杂算法来压缩视频文件。有一个多核CPU可用。

  • 计算可用CPU核心:使用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); // 根据需要进行调整

        ExecutorService threadPool = Executors.newFixedThreadPool(numberOfThreads);

        // 将视频编码任务提交到线程池。
        for (int i = 0; i < 10; i++) {
            threadPool.execute(() -> {
                encodeVideo(); // 模拟视频编码任务
            });
        }

        threadPool.shutdown();
    }

    private static void encodeVideo() {
        // 模拟视频编码(CPU 密集型)任务。
        // 这里进行复杂计算和压缩算法。
    }
}

确定I/O密集型任务的线程数量

对于I/O密集型任务,最佳线程数通常由I/O操作的性质和预期延迟决定。您希望拥有足够的线程以保持I/O设备繁忙而不使其过载。理想的数量不必等于CPU核心的数量。考虑构建一个网页爬虫,下载网页并提取信息。这涉及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; // 估计的 I/O 延迟(毫秒)
        int numberOfThreads = 4; // 根据预期延迟和系统能力进行调整

        ExecutorService threadPool = Executors.newFixedThreadPool(numberOfThreads);

        // 要抓取的 URL 列表。
        String[] urlsToCrawl = {
            "https://example.com",
            "https://google.com",
            "https://github.com",
            // 在这里添加更多 URL
        };

        for (String url : urlsToCrawl) {
            threadPool.execute(() -> {
                crawlWebPage(url, expectedIOLatency);
            });
        }

        threadPool.shutdown();
    }

    private static void crawlWebPage(String url, int expectedIOLatency) {
        // 模拟网页抓取(I/O 密集型)任务。
        // 执行 HTTP 请求并处理页面内容。
        try {
            Thread.sleep(expectedIOLatency); // 模拟 I/O 延迟
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

5.总结公式

确定线程池大小的公式可以写成如下形式。

线程数=可用核心数*目标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个线程。

责任编辑:武晓燕 来源: Java学研大本营
相关推荐

2010-03-18 15:15:08

Java线程池

2019-09-09 09:50:27

设置Java线程池

2023-08-04 11:04:03

线程池项目开发

2010-03-15 16:56:16

Java线程池

2023-09-08 12:19:01

线程方法interrupt

2024-03-15 09:32:47

线程池应用程序性能

2012-05-15 02:18:31

Java线程池

2021-09-23 15:55:50

线程池语言公式

2021-09-11 15:26:23

Java多线程线程池

2023-06-08 07:48:03

Java线程池

2015-08-20 09:17:36

Java线程池

2021-06-24 08:02:35

线程池Java代码

2022-02-28 07:01:22

线程中断interrupt

2010-02-24 10:24:10

Python线程

2023-11-22 08:37:40

Java线程池

2022-03-30 08:54:21

线程 Thread判断线程池任务Java

2023-07-05 07:48:04

线程池join关闭状态

2017-05-04 16:33:58

Java线程池实践

2023-09-19 14:59:47

线程开发

2020-09-04 10:29:47

Java线程池并发
点赞
收藏

51CTO技术栈公众号