线程池系统设置最全指南!

开发 前端
Target CPU utilization: 这是你希望你的应用程序使用的CPU时间的百分比***。如果设置目标CPU利用率过高,你的应用程序可能会变得无响应*。如果设置得太低,你的应用程序将无法充分利用可用的CPU资源。

Java中的线程创建会带来显著的开销。创建线程消耗时间,增加了请求处理的延迟,并涉及JVM和操作系统的大量工作。为了减轻这些开销,引入了线程池的概念。

在本文中,我们深入探讨确定理想线程池大小的艺术。一个经过精心调整的线程池可以从系统中提取出最佳性能,并帮助你在高峰工作负载中优雅地导航。然而,必须记住,即使使用线程池,线程的管理本身也可能成为瓶颈。

1 使用线程池的原因

  • 性能:线程的创建和销毁可能很昂贵,尤其是在Java中。线程池通过创建可以重复用于多个任务的线程池来减少这种开销。
  • 可伸缩性:线程池可以根据应用程序的需求进行扩展。例如,在负载较重时,线程池可以扩展以处理额外的任务。
  • 资源管理:线程池可以帮助管理线程使用的资源。例如,线程池可以限制在任何给定时间可以活动的线程数,这有助于防止应用程序耗尽内存。

2 设置线程池大小:了解系统和资源限制

在确定线程池大小时,了解系统的限制,包括硬件和外部依赖项,是至关重要的。让我们通过一个例子详细说明这个概念:

2.1 情景

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

2.2 考虑的因素

数据库连接池:假设你正在使用像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速度,而不是I/O设备的速度。

  • 编码或解码音频或视频文件
  • 编译和链接软件
  • 运行复杂的模拟
  • 执行机器学习或数据挖掘任务
  • 玩视频游戏

3.1  优化:

  • 多线程和并行性:并行处理是一种技术,用于将较大的任务分解为较小的子任务,并将这些子任务分配给多个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密集型任务是

那些与存储设备交互(例如,读/写文件),网络套接字(例如,发起API调用)或用户输入(例如,图形用户界面中的用户交互)的任务。

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

3.2 优化

  • 缓存:在内存中缓存经常访问的数据,以减少重复的I/O操作。
  • 负载平衡:将I/O密集型任务分布在多个线程或进程中,以有效处理并发的I/O操作。
  • 使用SSD:固态硬盘(SSD)可以显著加速I/O操作,与传统的硬盘驱动器(HDD)相比。
  • 使用高效的数据结构,例如哈希表和B树,以减少所需的I/O操作次数。
  • 避免不必要的文件操作,例如多次打开和关闭文件。

4  确定线程数量

4.1 CPU密集型任务:

对于CPU绑定的任务,你希望在不过分负担系统的情况下最大化CPU利用率,过多的线程可能导致过多的上下文切换,从而降低性能。一个常见的经验法则是使用可用的CPU核心数。

视频编码

想象一下,你正在开发一个视频处理应用程序。视频编码是一个CPU密集型任务,你需要对视频文件应用复杂的算法进行压缩。你有一个多核CPU可用。

为CPU绑定的任务确定线程数:

  1. 计算可用CPU核心数:使用**Runtime.getRuntime().availableProcessors()**在Java中确定可用CPU核心数。假设你有8个核心。
  2. 创建线程池:创建一个线程池,其大小接近或略小于可用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绑定)任务。
        // 复杂的计算和压缩算法在这里。
    }
}

4.2 对于I/O密集型任务

对于I/O绑定的任务,理想的线程数通常取决于I/O操作的性质和预期的延迟。你希望有足够的线程使I/O设备保持繁忙,而不会过载它们。理想的数字可能不一定等于CPU核心数。

网页爬取

考虑构建一个下载网页并提取信息的网络爬虫。这涉及进行I/O绑定的任务,由于网络延迟,可能需要发出HTTP请求。

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

  1. 分析I/O延迟:估计预期的I/O延迟,这取决于网络或存储。例如,如果每个HTTP请求大约需要500毫秒完成,你可能希望为I/O操作中的一些重叠提供一些余地。
  2. 创建线程池:创建一个线程池,其大小在并行性与预期的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 是否可以遵循一个具体的公式?

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

Number of threads = Number of Available Cores * Target CPU utilization * (1 + Wait time / Service time)

Number of Available Cores: 这是你的应用程序可用的***CPU核心数***。重要的是要注意,这与CPU数不同,因为***每个CPU可能有多个核心。***

Target CPU utilization: 这是你希望你的应用程序使用的CPU时间的百分比***。如果设置目标CPU利用率过高,你的应用程序可能会变得无响应*。如果设置得太低,你的应用程序将无法充分利用可用的CPU资源。

Wait time: 这是***线程等待I/O操作完成的时间***。这可能包括***等待网络响应、数据库查询或文件操作。***

Service time: 这是***线程执行计算的时间***。

Blocking coefficient: 这是等待时间与服务时间的比率。这是衡量线程等待I/O操作完成所花费的时间与执行计算所花费的时间之间关系的指标。

6 示例

假设你有一台具有4个CPU核心的服务器,并且你希望你的应用程序使用可用CPU资源的50%。

你的应用程序有两类任务: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个线程。

责任编辑:武晓燕 来源: JavaEdge
相关推荐

2021-06-17 06:57:10

SpringBoot线程池设置

2019-09-09 09:50:27

设置Java线程池

2018-03-27 09:31:21

数据库MySQL线程池

2023-05-19 08:01:24

Key消费场景

2020-12-10 08:24:40

线程池线程方法

2012-05-15 02:18:31

Java线程池

2015-12-16 10:30:18

前端开发指南

2024-02-04 08:26:38

线程池参数内存

2022-03-23 07:54:05

Java线程池系统

2023-10-13 08:20:02

Spring线程池id

2023-06-07 13:49:00

多线程编程C#

2019-12-27 09:09:42

Tomcat线程池JDK

2017-01-10 13:39:57

Python线程池进程池

2020-03-05 15:34:16

线程池C语言局域网

2012-02-29 13:26:20

Java

2020-09-04 10:29:47

Java线程池并发

2011-06-22 15:50:45

QT 线程

2013-05-28 13:57:12

MariaDB

2010-07-20 16:13:25

Perl线程

2023-01-07 17:41:36

线程池并发
点赞
收藏

51CTO技术栈公众号