怎么在JAVA中确定线性池大小

描述

在JAVA中确定线性池大小,分别介绍CPU密集型任务和I/O密集型任务及其处理方法

在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//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学研大本营公众号

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分