不能放弃任务,线程池的任务拒绝策略应该如何设定?

296 阅读10分钟

1.线程池的拒绝策略和异常捕获

1.1什么时候要进行用到拒绝策略?

就是当所有线程都在工作的时候,并且任务队列已经满了,系统无法处理这么多请求了就要去进行触发拒绝策略。

1.2JDK内置的拒绝策略

JDK提供了四种内置的拒绝策略:

1.DiscardPolicy:默默丢弃无法处理的任务,不予任何处理。

2.DiscardOlderstPolicy:丢弃队列中最老的任务,尝试再次提交当前任务。

3.AbortPolicy(默认):直接抛出异常,组织系统正常工作。

4.CallerRunsPolicy 将任务分给调用线程来执行,运行当前被丢弃的任务,这样做不回真的丢去任务,但是提交线程的性能可能会急剧下降(主线程被占用了,会产生同步阻塞)。

CallerRunsPolicy使用的是当前的调用执行异步任务的线程进行执行被拒绝的任务,所以进行阻塞的也是当前启动异步任务的线程,会影响程序的整体性能,如果你的程序可以承受次延迟并且你要求任何一个任务都要被执行的话,你可以选择这个任务 => 问题,任务多了就是一直阻塞在这个线程,还是比较糟糕的,和把任务队列调大没有任何区别。其实进行理解就是,CallerRunsPolicy会将任务会退给调用者,让调用者进行处理。

1.3创建线程池的时候指定拒绝策略

new ThreadPoolExecutor(corePoolSize活跃线程数,maximumPoolSize全部可工作线程数,keepAliveTime多久后销毁临时线程,TimeUnit时间的单位,queue任务队列,线程池拒绝策略)

活跃线程数就是正式员工,全部可工作线程数就是可以进行同时工作的线程,任务队列就是存储任务的,拒绝策略就是线程池沾满了任务队列也满了的情况,如何进行处理又来的任务。

1.4自定义一个拒绝策略

将初始化的拒绝策略传到ThreadPoolExecutor中,就可以配置自定义策略。

1738766933503.png

2.拒绝策略的选择

2.1如果不允许丢弃任务,应该选择哪个拒绝策略

如果不允许丢弃任务,在JDK提供的四个默认的拒绝策略中

DiscardPolicy:默认丢弃无法处理的任务,不做任何处理。

DiscardOldersPolicy:丢弃任务队列中最老进入的任务,尝试重新提交任务。

AbortPolicy(默认策略):丢弃无法处理的任务,抛出异常,中断当前程序的执行。

CallerRunsPolicy:将无法处理的任务抛回给当前调用线程,进行执行这个无法被处理的任务。

目前可以很容想到使用CallerRunsPolicy:从源码中可以卡看到,只要当前程序不关闭,就会使用execute方法的线程执行该任务。

1738766976327.png

2.2CallerRunsPolicy拒绝策略有什么风险吗?如何解决?

1.如果走到CallerRunsPolicy的是一个十分消耗时间的任务,而且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行,严重情况下可能会导致OOM。

注明:OOM的全程是Out Of Memory,翻译为中文就是内存用完啦,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收期也已经没有空间可以进行回收的时候,就会出现这个错误。

image.png

2.如果我们的目标是使用线程池进行控制系统流量,比如我们现在设置的线城市核心线程有两个,最大线程4个,我们有一个AI服务,最多允许四个人同时调用,现在线程池所有线程都在运作,并且任务队列也满了,那如果我为了保证任务不丢弃,继续执行,调用主线程执行AI任务,那AI服务就直接超负荷崩掉了。

image.png

2.3那如何保证任务不被丢失呢?=> 任务持久化

如果服务器资源已经达到了可利用的极限,这意味着我们要在设计策略上改变线程池的调度了,我们都知道,导致主线程卡死或者AI服务(任何一个有限度的服务)崩溃的根本原因就是我们不希望任何一个任务被丢弃,换个思路,我们能不能在保证任务不丢失的情况下还能在服务器有余力的时候去处理呢?

那就是进行数据持久化操作。

1.可以设计一个任务表存储到MySQL数据库中

2.Redis缓存数据

3.Caffeint本地缓存数据

4.将任务提交到消息队列中

2.4数据库持久化方案的实现 => 也适用于缓存数据库方案

1.实现RejectedExecutionHandler接口自定义拒绝策略,自定义拒绝策略可以负责将线程池暂时无法处理(此时任务队列已满)的任务入库(保存到MySQL中)。注意:线程池在那时无法处理的任务会被先放在任务队列中,任务队列满了才会触发拒绝策略。

2.继承BlockingQueue实现一个混合式任务队列,该队列包含JDK自带的ArrayBlockingQueue。这里是这样的,建议先取早进入的任务,需要进行重写take()拿取任务的方法,先去数据库中读取最早的任务,数据库中没有任务的时候再去ArrayyBlockQueue中进行拿任务。

3.方案的问题思考以及优化

3.1问题分析

虽然我们成功持久化了数据,但是我们优先取数据库中拿数据,这个方案会带来的问题就是,我们很有可能拿不到最早加入队列的任务,因为最早加入的任务很有可能在任务队列中,而我们是先去处理数据库存储的任务了,还有一种极端情况,任务不断来,数据库中任务数据不断增加,然而你任务队列中的数据一直不取,那最早进任务队列里的几个数据不就死里面了吗?

当然也可以阻止任务入队,只走数据库持久化存储任务。

但是现在给大家提供一种新思路,自己去把控持久化数据。

3.2场景搭建

目前有一个场景:目前有一个场景,我们需要调用AI执行一个耗时任务,并且我们AI处理的能力很有限(GPU资源不足),而且现在我们不能遗漏任何一个任务,所有请求都要求成功,那我们应该怎么做呢?

3.3目标分析

首先我们必须进行分解一下目标:

1.我们需要保证AI不被压垮

2.我们需要保证用户不能跟个傻子样干等着

3.我们必须保证任何一个任务都不能丢弃

4.我们必须保证首先执行的一定是最早的任务

3.4方案设计

3.4.1线程池异步化

首先用户不能傻等着那一定得走异步,因为AI分析是一个很耗时间的任务,如果让用户在那里干等着,那用户就要掀桌子了,所以系统必须设计为异步的。

3.4.2线程池的设计

为什么需要专门设计一个线程池,当然是因为如果我们随便用一个线程池(或者用系统默认的),我们不能把控住到底创建了多少个异步任务,如果AI受不了这么多人调度呢?好家伙我AI就能抗住俩人同时调用,你直接给我整10个异步任务同时调度我,那AI不直接嘎掉了?所以我们必须进行设计一个符合我们当前业务的线程池。

假装我们和AI部门沟通,目前AI部门形式大差,老板穷的不行了,钱都买茅子了,现在就一台32G显存的GPU服务器了,AI部门说,这个AI推理能力比较慢,只能俩人同时用,而且每次调用得至少推理半分钟,根据你的经验,用户等一分钟都很强了,所以你决定,设置两个常备线程,最大两个线程,任务队列最多两个。但是此时老板又发威了,所有任务不能丢,一时处理不完要用短信发给用户说你的结果已经处理完了,请及时查看,这可把你愁坏了。

3.4.3线程池任务拒绝策略和持久化策略的设计

你决定创建一个任务表,这个任务表主要是用来存储所有任务的,一方面可以进行数据统计分析,一方面你要通过任务表进行存储很多AI一时半会处理不完需要持久化挂起的任务。

让我们来研究着个表,窗口ID是因为用户可能开多个AI窗口进行发布任务,任务就是进行存储执行的任务的,isNotify是最关键的一个,这个字段主要是进行存储是否要通知用户,需要通知的就是被挂起来的(当前AI一时半会整不完啊,没有加入到线程池中的全被挂起来了),不需要通知的就是一会AI就出结果的。

1738767036625.png

3.4.4线程池任务拒绝策略怎么搞?

我根据需求进行分析,选定了以下策略

1.当线程正常调度的时候,将任务加入到异步任务中进行调度使用

2.当线程池中的工作线程和任务队列都满了之后,进行返回一个任务被挂起的结果,然后将任务存储到数据库中,进行存储下来,等线程池空闲的时候去取出任务并执行。

3.在调度异步任务之前,进行判断一下数据库中是否还有没有执行过的任务。如果没有就将调度线程池执行异步任务,如果有,就将异步任务挂起到数据库中,并返回执行结果。

4.使用定时任务进行定时轮询线程池的任务队列是否有空闲(判断是否工作线程都满了,并且任务队列空闲,也就是 2 =< Task =< 3),如果有空闲,就调度线程池执行任务。

3.5方案实现

3.5.1扩展Runnable => 方便存储上下文信息,方便rejectedExecution可以取到 按自己需求进行扩展即可

package com.langchao.ai.config;

import lombok.Data;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.Serializable;

@Data
public class CallRunable implements Runnable, Serializable {

private Runnable task;
private String content;
private Long chatWindowsId;
private Long userId;
private SseEmitter sseEmitter;

public CallRunable(Runnable task, Long chatWindowsId, Long userId, String content) {
    this.task = task;
    this.chatWindowsId = chatWindowsId;
    this.userId = userId;
    this.content = content;
}

public CallRunable(Runnable task, Long chatWindowsId, Long userId, String content, SseEmitter sseEmitter) {
    this.task = task;
    this.chatWindowsId = chatWindowsId;
    this.userId = userId;
    this.content = content;
    this.sseEmitter = sseEmitter;
}

@Override
public void run() {
    task.run();
}

}

3.5.2实现线程池拒绝策略

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.langchao.ai.model.entity.RejectTask;
import com.langchao.ai.service.RejectTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import javax.annotation.Resource;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
* 自定义线程池拒绝策略
*/
@Slf4j
@Component
public class ThreadPoolRejectionHandler implements RejectedExecutionHandler {

@Resource
private RejectTaskService rejectTaskService;

@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    if (r instanceof CallRunable) {
        // 1. 拿到上下文信息
        // 伪代码
        
        // 进行执行持久化任务
        QueryWrapper<RejectTask> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("userId", userId);
        queryWrapper.eq("chatWindowId", chatWindowsId);
        RejectTask task = rejectTaskService.getOne(queryWrapper);
        // 更新任务
        task.setTask(content);
        task.setIsNotify(0);
        rejectTaskService.updateById(task);
    } else {
        System.out.println("Task was rejected, but it's not a CallRunnable instance.");
    }


    System.out.println("失败啦");
}
}

3.5.3实现线程池

import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Configuration
public class ThreadPoolExecutorConfig {

@Resource
private ThreadPoolRejectionHandler threadPoolRejectionHandler;

@Bean
public ThreadPoolExecutor threadPoolExecutor() {

    // 配置线程工厂
    ThreadFactory threadFactory = new ThreadFactory() {
        private int count = 1;

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

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 100, TimeUnit.SECONDS,
             new ArrayBlockingQueue<>(2), threadFactory, threadPoolRejectionHandler);
    return threadPoolExecutor;
 }
}

3.5.4业务代码(处理新请求)

// 校验/或者处理其他信息

// 将任务推进到数据库中
RejectTask rejectTask = new RejectTask();
rejectTask.setUserId(loginUser.getId());
rejectTask.setChatWindowId(chatWindowId);
rejectTask.setTask(content);
rejectTask.setIsNotify(1);
boolean isSuccess = rejectTaskService.save(rejectTask);
ThrowUtils.throwIf(!isSuccess, ErrorCode.SYSTEM_ERROR, "系统出错!");

// 异步化系统
CallRunable callRunable = new CallRunable(() -> {
 doAsyncAI.asyncUserAI(loginUser.getId(), chatWindowId, sseEmitter, chatWindows, rejectTask, content);
 }, chatWindowId, loginUser.getId(), content, sseEmitter);

 // 判断任务数据库中是否还有更早的任务
QueryWrapper<RejectTask> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("isNotify", 0);
RejectTask task = rejectTaskService.getOne(queryWrapper);
 if (task != null) {
sseEmitter.send("任务被挂起");
sseEmitter.complete();
return sseEmitter;
}

// 调度执行任务
threadPoolExecutor.execute(callRunable);

3.5.5配置定时任务

1738767240216.png 1738767269241.png