Java线程池的设计与使用

Java线程池的设计与使用

多线程情景引入

情景分析

  1. 请求积压的情况
    • 系统资源受限: 当大量用户请求同时到来时,服务器受限于内存、CPU、和网络带宽等资源,导致用户长时间等待。
    • 后端处理能力限制: 如频率限制措施(每秒或每几秒的访问限制)在高并发情况下会增加服务器压力,导致请求处理延迟。
    • 线程数限制: 例如,Tomcat服务器在高并发下(如200用户/秒)可能因线程数限制而导致请求积压,延迟数据处理。
  2. 流量突刺情况
    • 第三方服务能力限制: 如使用AI服务每三秒只能处理一个请求,在高并发下突然到来上百个请求,可能会导致AI过载,甚至服务拒绝。
    • AI处理能力限制: AI服务在面对大量同时请求时可能误判为攻击,导致服务限制或拒绝。

解决方法

  • 异步化策略: 识别并处理那些可能导致服务处理能力受限或处理时间过长的场景,如数据量大或第三方服务响应慢的情况。
  • 减少等待时间: 通过异步处理避免用户长时间等待,如在处理繁重任务时采用异步方式。

系统问题分析总结

  • 面对的问题:
    1. 用户等待时间过长: 主要由于AI结果生成的延迟。
    2. 服务器资源紧张: 高并发请求可能导致系统资源紧张,极端情况下可能造成服务器宕机。
    3. 第三方服务能力限制: 如AI服务的处理能力限制(例如每3秒处理1个请求)可能导致处理不及时。
  • 综合对策: 面对上述问题,采用异步化解决方案,优化系统处理能力和用户体验。

异步化

  1. 介绍
    • 同步与异步的对比:
      • 同步: 完成一件事情后才能开始另一件(如烧水后才能开始工作)。
      • 异步: 在处理一件事情的同时,可以进行另一件事情。一旦第一件事完成,系统会通过通知告知,从而进行后续处理(如在烧水的同时处理其他工作,水壶的蜂鸣器通知水烧好,可进行下一步)。
      • 通知机制: 异步化的关键在于知道何时任务已完成,需要一个有效的通知机制。
  2. 异步业务流程分析
    • 在异步流程中,用户提交请求后无需在界面等待,可直接返回主界面或继续其他操作。提交完成后,可以在主页上看到图表生成状态。
    • 消息通知功能: 用于告知用户任务完成情况,比如在界面右上角提供消息通知。
  3. 标准异步化业务流程
    • 流程实施:
      • 用户长时间操作时,提交请求后无需等待,系统先将请求保存至数据库。
      • 将用户任务加入任务队列,由程序或线程按顺序执行。
      • 任务队列类似备忘录,记录待处理事项,按资源可用性处理。
    • 处理策略:
      • 如果任务队列满或线程忙碌,可以选择直接拒绝任务,或记录下来待后续处理。
      • 无论成功与否,应将任务保存到数据库以供后期查阅。
      • 对于提交失败的任务,可在程序空闲时从数据库中提取并执行。
    • 任务状态更新:
      • 程序执行任务后,更新数据库中的任务状态。
      • 为用户提供查询任务状态的功能,以减少无尽等待。
    • 用户体验优化:
      • 异步执行适用于复杂分析,用户可提交新任务或实时查看状态,而非长时间等待。
      • 进度条等可视化工具可用于显示任务进度,提高透明度和用户体验。
  4. 标准异步化流程总结
    • 提交长时间操作请求后,任务保存至数据库,无需用户在界面等待。
    • 根据系统资源状况,任务可能立即执行、进入等待队列,或在失败时被记录待后续处理。
    • 系统从队列中取任务执行,并更新状态。
    • 用户可查询任务状态或接收完成通知(如邮件、系统消息)。

注意点

  • 选择异步化: 并非所有操作都需要异步化。只有在执行时间长的场景中考虑异步化,以避免增加代码复杂度和潜在问题。
  • 异步处理的复杂性: 异步执行中,开发者可能不清楚程序执行到哪一步,因此需要记录每个小任务的状态或进度。
  • 用户体验: 对于复杂任务,提供进度条等可视化工具,以便用户了解任务执行情况,优化体验。

异步化流程与线程池概念

  1. 异步化流程详解

    • 线程角色分配: 设想我们的程序有一个工作者,即线程,例如称之为线程A。此外,存在一个待处理任务的队列。
    • 任务处理: 用户提交任务(如智能分析任务1),任务加入队列。线程小季负责从队列中取出任务执行,但由于线程处理能力有限,一次只能处理一个任务。
    • 多任务情况: 若有第二个任务(任务2)提交,而线程A正在处理任务1,则需考虑调用另一个线程(比如线程B)来处理。如果有更多任务,而可用线程已满,新任务将排队等待处理。
    • 队列溢出处理: 当任务队列满时,为维护系统稳定性,新任务会被记录到数据库中,但暂时不加入队列,待有空闲线程时再加入处理。
    • 任务分配策略: 如何分配任务至线程(比如线程ABCD-Z)需根据实际情况和处理速度来决定。例如,线程A处理速度快,可连续处理多个任务,而线程B处理较慢。、

    线程与任务队列的可视化图解,如下图所示:

    任务加入队列
    线程A可用
    线程B可用
    任务完成
    任务完成
    队列满时
    线程空闲时
    用户提交任务
    任务队列
    线程A处理任务
    线程B处理任务
    返回结果给用户
    记录任务至数据库

    包含以下步骤:

    1. 用户提交任务: 用户的任务请求首先被提交。
    2. 任务加入队列: 提交的任务会被加入到待处理的任务队列中。
    3. 线程处理任务: 根据线程的可用性(例如线程A和线程B),从队列中取出任务进行处理。
    4. 返回结果: 线程完成任务后,结果会返回给用户。
    5. 队列溢出处理: 当任务队列满时,新提交的任务会被记录到数据库中,而不是直接加入队列。
    6. 任务再分配: 当有线程变为空闲状态时,可以从数据库中取出之前记录的任务,重新加入队列处理。
  2. 线程池总结

    • 线程管理的复杂性: 管理线程(如何新增或减少线程)和任务处理(何时接收或拒绝任务)是复杂的。
    • 线程池作用: 线程池协助管理线程,调整任务执行流程,确保高效协调。
    • 线程池的灵活性: 可根据需求设定线程池的最大线程数,线程池会根据任务紧急程度或线程空闲状态来分配任务。
  3. 线程池的实现和应用

    • 实现挑战: 自行实现线程池涉及多方面的考虑,例如何时增加或减少线程,如何防止多个线程抢占同一任务等。
    • 协调与任务窃取: 在Linux环境中,存在一种任务窃取的机制,例如线程A效率高可接手线程B的任务,这需要线程间的有效协调。
    • 数据结构的选择: 实现线程池时,所选用的数据结构(如阻塞队列)对线程管理策略和任务分配具有重要影响。
  4. 注意事项

    • 任务队列的设计: 要考虑任务队列的最大容量和处理策略,以防溢出时影响系统性能。
    • 线程池大小的选择: 依据系统负载和任务特性调整线程池大小,避免资源浪费或处理瓶颈。
    • 任务分配策略: 合理分配任务至线程,考虑任务的紧急程度和线程的处理能力,以提高效率。

线程池的选用与学习

  1. 在Spring框架中实现线程池
    • 使用ThreadPoolTaskExecutor@Async注解: 在Spring框架中,可以通过ThreadPoolTaskExecutor结合@Async注解来实现线程池。这种方法虽然可行,但有一些局限。
    • 局限性: Spring作为一个全面的框架,对线程池进行了封装,这可能隐藏了一些底层细节。对于初学者来说,这种封装可能不利于深入理解线程池的工作原理。
  2. 直接使用Java并发编程包
    • 推荐使用ThreadPoolExecutor: 直接使用Java中的并发编程包,特别是ThreadPoolExecutor,可以实现更灵活和详细的线程池定制。这种方法更适合于需要深入了解和控制线程行为的场景。
    • Java并发编程包的学习建议: 建议在学完Spring Boot并能够实现一个项目,以及在学习完Redis之后,再系统地学习Java并发编程包(Java Concurrency Utilities, JUC)。这样的顺序可以帮助避免过早的学习压力,让学习者在有了一定的实践基础后,更好地理解并发编程的概念和应用。

实践建议

  • 逐步学习: 并发编程是一个复杂且强大的工具,适合在掌握了基础的编程技能和框架使用后进一步学习。
  • 理论与实践相结合: 在学习并发编程的过程中,结合具体的项目实践,可以更加深刻地理解并发控制的细节和挑战。
  • 了解底层实现: 尽管框架如Spring提供了便利的封装,但理解其底层实现(如直接使用Java的并发工具)有助于深入理解并发机制。

Java-JUC并发编程-线程池学习与使用

线程池参数详解

ThreadPoolExecutor 主要有以下几个参数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

Java线程池参数的详细讲解:

  1. corePoolSize(核心线程数)
    • 描述:核心线程数类似于公司的正式员工,随时准备处理任务。
    • 作用:保证即使在负载较低时,也有一定数量的线程活跃以应对突发任务。
    • 设定原则:根据允许同时进行的任务数来设定。例如,如果AI服务允许4个任务同时执行,核心线程数应设为4。
  2. maximumPoolSize(最大线程数)
    • 描述:最大线程数定义了线程池在极限情况下的线程数量上限。
    • 作用:确保在高负载情况下,线程池不会超过一定的资源限制。当线程数 = maximumPoolSize, 且任务队列已满,此时添加任务时会触发RejectedExecutionHandler进行处理
    • 设定原则:考虑成本和资源限制。例如,如服务最多允许4个任务同时执行,则最大线程数应设为4。
  3. keepAliveTime(空闲线程存活时间)
    • 描述:这个参数决定了非核心线程(即临时线程)在空闲时会等待多久才被终止。
    • 作用:管理线程池的规模,释放不再需要的线程资源。
    • 设定原则:根据任务频率和资源管理需求来设定,以避免过多占用资源。
  4. TimeUnit(时间单位)
    • 描述:用于设定keepAliveTime参数的时间单位,例如分钟、秒等。
    • 作用:提供灵活性,以便根据具体场景选择合适的时间单位。
  5. workQueue(任务队列)
    • 描述:存储待执行任务的队列,也称为阻塞/工作队列。
    • 作用:缓存待处理任务,确保按顺序执行。
    • 设定原则:需要设定队列长度,因为无限长度的队列可能会消耗过多系统资源。
  6. threadFactory(线程工厂)
    • 描述:负责生成新线程的工厂,类似于公司的人力资源部门。
    • 作用:允许自定义线程的创建,如设定线程名称、优先级等。
  7. RejectedExecutionHandler(拒绝策略)
    • 描述:定义当任务队列满时如何处理新来的任务,例如抛出异常或采用其他策略。
    • 应用:可以设定任务优先级,或者使用资源隔离策略(例如,分别为VIP任务和普通任务设立不同的线程池)。

参数设置参考建议

任务类型考虑

  • 计算密集型任务: 对于CPU密集型任务(如视频处理、图像处理等),corePoolSize建议设置为CPU核数+1,以最大化CPU利用率,减少线程切换。
  • I/O密集型任务: 对于带宽、内存或硬盘读写密集的任务,corePoolSize可以设置得相对较大,一般经验值为2倍CPU核数,但应以实际I/O能力为准。

工作队列(workQueue)的详细描述

在Java线程池中,workQueue是用于存储等待执行的任务的队列。它是线程池的一个重要组成部分,影响着线程池的任务处理策略。以下是几种常见的工作队列类型及其特点:

  1. ArrayBlockingQueue - 有界队列

    • 特点: 这是一个基于数组结构的有界阻塞队列,需要在创建时指定队列的大小。
    • 适用场景: 当希望线程池处理任务的数量有一个明确的上限时,以避免资源耗尽。
    • 有界队列(如ArrayBlockingQueue)可以防止资源耗尽,但可能导致新任务在队列满时被拒绝。
  2. LinkedBlockingQueue - 链表队列

    • 有界与无界:
      • 如果在创建时指定了大小,它表现得与ArrayBlockingQueue相似。
      • 如果未指定大小,它将变成一个实际上的无界队列,其最大容量为Integer.MAX_VALUE
    • 适用场景: 适用于任务处理不需要严格的数量限制,或者当希望队列能够自动扩容以处理更多的任务时。
    • 无界队列(如未指定大小的LinkedBlockingQueue)可以减少任务拒绝的风险,但可能会导致系统资源耗尽,尤其是在任务提交速度远大于处理速度的情况下。
  3. SynchronousQueue - 同步阻塞队列

    • 特点: 这个队列没有实际的容量。每一个插入操作必须等待一个相应的删除操作,反之亦然。
    • 适用场景: 适用于任务处理需要即时响应的场景。例如,newCachedThreadPool就使用这种队列。
    • 同步队列(如SynchronousQueue)适用于任务执行需要立即处理的场景,但可能会对线程池的工作效率产生影响。

ThreadFactory - 创建线程的工厂

ThreadFactory 是一个接口,允许你在Java线程池中自定义线程的创建方式。通过实现ThreadFactory,可以在创建线程时执行一些特定操作,例如自定义线程的名称、优先级、是否为守护线程等。以下是ThreadFactory的一个自定义实现示例:

private static class CustomThreadFactory implements ThreadFactory {
    private final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    CustomThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = "ume-pool-" + POOL_NUMBER.getAndIncrement() + "-thread-";
    }

    @Override
    public Thread newThread(@NonNull Runnable runnable) {
        Thread thread = new Thread(group, runnable, namePrefix + threadNumber.getAndIncrement(), 0);
        if (thread.isDaemon()) {
            thread.setDaemon(false);
        }
        if (thread.getPriority() != Thread.NORM_PRIORITY) {
            thread.setPriority(Thread.NORM_PRIORITY);
        }
        return thread;
    }
}

RejectedExecutionHandler - 拒绝策略

当线程池中的线程数达到最大值且工作队列已满时,RejectedExecutionHandler定义了线程池如何处理无法执行的新提交任务。RejectedExecutionHandler 是一个接口,定义了线程池如何处理这些不能执行的任务。以下是一些标准的实现:

  • AbortPolicy
    • 实现: 这是默认的拒绝策略。当任务被拒绝时,它会抛出RejectedExecutionException异常。
    • 应用场景: 适用于那些希望在任务超出处理能力时立即得知的场合。这种策略可以迅速反馈系统过载的问题。
  • CallerRunsPolicy
    • 实现: 此策略并不抛弃任务,也不抛出异常。相反,它会将任务回退到调用者,从而在调用者的线程中运行任务。
    • 应用场景: 适用于在任务被拒绝时仍希望任务得以执行的场合。这种方式减少了新任务的丢弃,但增加了调用者线程的负载。
  • DiscardPolicy
    • 实现: 任务被拒绝时,该策略将直接丢弃任务,而不会有任何动作。
    • 应用场景: 当任务可以被安全丢弃时使用。这种策略适用于那些对丢弃任务不敏感的场合。
  • DiscardOldestPolicy
    • 实现: 该策略将丢弃最早的未处理任务(即队列中最长时间的任务),然后尝试重新提交新的任务。
    • 应用场景: 适用于希望牺牲部分旧任务以获取新任务处理机会的场景。这种方式试图通过替换旧任务来为新任务腾出空间。
选择合适的策略

在选择合适的RejectedExecutionHandler实现时,需要考虑以下因素:

  • 任务的重要性: 如果每个任务都非常重要,不能被丢弃,那么CallerRunsPolicy可能是一个更好的选择。
  • 资源限制: 如果系统资源(如内存和CPU)非常有限,可能需要选择DiscardPolicyDiscardOldestPolicy来避免资源过载。
  • 性能要求: 如果系统性能是关键考虑因素,AbortPolicy可以迅速反馈系统过载的问题,但可能需要额外的错误处理逻辑。
策略的实现

以下是Java线程池中几种标准拒绝策略的参考代码实现:

  1. AbortPolicy

    这是默认的拒绝策略,抛出RejectedExecutionException异常。

    RejectedExecutionHandler abortPolicyHandler = new ThreadPoolExecutor.AbortPolicy();
    
  2. CallerRunsPolicy

    这种策略将任务回退到调用者的线程中运行。

    RejectedExecutionHandler callerRunsPolicyHandler = new ThreadPoolExecutor.CallerRunsPolicy();
    
  3. DiscardPolicy

    这种策略将直接丢弃任务,而不会有任何动作。

    RejectedExecutionHandler discardPolicyHandler = new ThreadPoolExecutor.DiscardPolicy();
    
  4. DiscardOldestPolicy

    这种策略将丢弃队列中最早的未处理任务,然后尝试重新提交新的任务。

    RejectedExecutionHandler discardOldestPolicyHandler = new ThreadPoolExecutor.DiscardOldestPolicy();
    
  5. 自定义RejectedExecutionHandler

    自定义的拒绝策略可以根据具体需要进行实现,例如下面这个示例记录了被拒绝的任务。

    private static class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
        private CustomRejectedExecutionHandler() {}
    
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            Log.e("umeweb", "Task " + r.toString() + " rejected from " + e.toString());
        }
    }
    

在使用这些策略时,可以将它们作为参数传递给ThreadPoolExecutor的构造函数,以定义线程池如何处理超出其容量和队列限制的任务。

线程池工作原理

假设我们的设置的参数:

  • corePoolSize: 2

  • maximumPoolSize: 4

  • workQueue.size = 2

  1. 最初开始的时候,没有任何线程也没有任何任务添加进任务队列里
image-20240109164743799
  1. 来了一人任务,发现我们的员工还没有达到正式员工数(corePoolSize = 2),来一个员工直接处理这个任务.
image-20240109165159731

又来了一个任务,发现我们的员工还没有达到正式员工数(corePoolSize = 2),再来一个员工直接处理这个任务.

image-20240109165318311

又来了一个任务,但是我们正式员工数已经满了(当前线程数 = corePolSize = 2),任务放到队列(最大长度workQueue.size 是 2) 里等待,而不是再加新员工。

image-20240109165439790

又来了一个任务,但是我们的任务队列已经满了(当前线程数> corePoolSize = 2,已有任务数 = 最大长度workQueue.size = 2) ,新增线程 (maximumPoolSize = 4)来处理新任务,而不是丢弃任务

image-20240109165910222

已经到了任务7,但是我们的任务队列已经满了、临时工也招满了(当前线程数 = maximumPoolSize = 4,已有任务数 = 最大长度,workQueue.size = 2),调用 RejectedExecutionHandler 拒绝策略来处理多余的任务

image-20240109170047531

如果当前线程数超过 corePoolSize (正式员工数),又没有新的任务给他,那么等 keepAliveTime 时间达到后就可以把这个线程释放。

线程池使用方法

此类中定义了线程池的核心参数,包括核心线程数、最大线程数、空闲线程的存活时间、时间单位和任务队列。threadPoolExecutor方法创建并返回一个配置好的ThreadPoolExecutor实例。自定义线程工厂threadFactory用于创建具有特定名称模式的线程,而AbortPolicy作为拒绝策略在任务被拒绝时抛出异常。这些配置确保了线程池的高效和合理管理,同时提供了足够的灵活性来处理各种任务。

package com.caixy.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.validation.constraints.NotNull;
import java.util.concurrent.*;

/**
 * 线程池配置
 *
 * @name: com.caixy.backend.config.ThreadPoolExecutorConfig
 * @author: CAIXYPROMISE
 * @since: 2024-01-09 19:36
 **/
@Configuration
public class ThreadPoolExecutorConfig
{
    // 核心线程数 - 线程池保持活跃的线程数
    private final int corePoolSize = 10;

    // 最大线程数 - 线程池最大能创建的线程数
    private final int maximumPoolSize = 20;

    // 空闲线程存活时间 - 当线程数大于核心线程数时,这是多余空闲线程在终止前的最大存活时间
    private final long keepAliveTime = 60;

    // 时间单位 - 上述存活时间的时间单位
    private final TimeUnit unit = TimeUnit.SECONDS;

    // 队列容量 - 存放待执行任务的队列容量
    private final int queueCapacity = 50;

    // 工作队列 - 用于存放待执行的任务
    private final ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(queueCapacity);

    /**
     * 配置并返回一个ThreadPoolExecutor线程池。
     * 此方法创建一个新的ThreadPoolExecutor并配置其参数
     *
     * @return 配置好的ThreadPoolExecutor实例
     */
    @Bean
    public ThreadPoolExecutor threadPoolExecutor()
    {
        // 自定义线程工厂 - 用于创建新线程
        ThreadFactory threadFactory = new ThreadFactory()
        {
            private int count = 0;

            @Override
            public Thread newThread(@NotNull Runnable r)
            {
                Thread thread = new Thread(r);
                // 设置线程名称
                thread.setName("thread-" + count++);
                return thread;
            }
        };

        // 创建并返回ThreadPoolExecutor实例
        return new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 - 当线程池无法接受任务时抛出异常
        );
    }
}
创建线程池异步任务
@RestController
@RequestMapping("/thread")
@AllArgsConstructor
public class ThreadController
{
    // 注入线程池
 	private final ThreadPoolExecutor threadPoolExecutor;
    
    @GetMapping("/put")
    public String putThread()
    {
        CompletableFuture.runAsync(() -> {
        log.info("{} to do something!!", Thread.currentThread().getName());
            try
            {
                Thread.sleep(1000);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            log.info("{} finished!!", Thread.currentThread().getName());
        }, threadPoolExecutor);
        return "Task submitted successfully";
    }
}

CompletableFuture.runAsync(Runnable runnable, Executor executor) 是Java 8引入的CompletableFuture类的一个方法。它用于异步执行一个任务,这个任务不返回任何值(即Runnable接口的一个实例)。该方法提供了一种在将来某个时间点完成任务执行的方式,而不会阻塞当前线程。以下是该方法的关键特征:

  1. 异步执行: runAsync方法会在一个单独的线程(从指定的Executor中获取)上异步执行给定的Runnable任务。这意味着方法调用会立即返回,而实际的任务执行会在另一个线程中进行。
  2. 任务类型: 作为参数的Runnable接口代表没有返回值的任务。如果您有返回值的异步任务,应该考虑使用CompletableFuture.supplyAsync(Supplier<U>, Executor)
  3. 使用指定的Executor: 您可以指定一个Executor来执行任务,这提供了对执行环境更多的控制。例如,可以指定一个线程池来管理任务的执行。如果不指定Executor,则会使用ForkJoinPool.commonPool()作为默认执行器。
  4. 返回值: 方法返回一个CompletableFuture<Void>对象。这个CompletableFuture可以用来检查任务是否完成(成功或失败),并允许您在任务完成后添加进一步的操作,如使用thenAcceptthenRunexceptionally方法。

结语

如果您喜欢我们的文章,请不要忘记点击关注。我们将继续推出更多关于计算机视觉、人工智能、以及C++、Python、Java等技术领域的精彩内容。您的支持是我们不断前进、分享更多知识和见解的最大动力。我们期待与您一起探索这些激动人心的技术领域,共同成长。感谢您的阅读和支持,敬请期待我们的后续文章!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值