潜用多线程技术解决项目并行执行问题

功能模拟说明

一个很长的集合,集合中存储的是要远程调用相应API的参数,类似于:

java 复制代码
Arrays.asList("参数1","参数2", "参数3","参数4","参数5",...);

API长这样:

https 复制代码
Get: https://....../search?parm=参数1,参数2,参数3...

好,我摊牌了,

4800家的A股上市公司,需要查询每家上市公司的相应股票数据,查询的API接口参数拼接的参数就是每家个股的编码,首先需要解决的问题是,Get的请求路径长度拼接不了那么长的参数列表,怎么办?

集合切片

引入guava工具包:

xml 复制代码
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.2-jre</version>
</dependency>

模拟测试:

java 复制代码
// 准备测试数据
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
    list.add(i);
}

// 切片
List<List<Integer>> partition = Lists.partition(list, 15);

// 查看
partition.forEach(System.out::println);

循环切片集合,拼接切片集合中每个元素的数据去请求,就解决了Get请求路径太长的问题。

好了,以上只是记录一下长集合切片处理的过程,接下来,真正的记录多线程处理功能的细节。

串行功能演示

参数集合长度为25,循环集合,循环中模拟程序执行延迟时间为1到2秒随机,查看程序执行效果。

java 复制代码
// 集合数据模拟
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 25; i++) {
    list.add(i);
}

// 循环执行业务
for (int i = 0; i < list.size(); i++) {
    // 模拟业务延迟时间
    int time = (new Random().nextInt(2) + 1) * 1000;
    Thread.sleep(time);
    log.info("线程:{},执行业务任务id:{},耗时:{} 毫秒完成", Thread.currentThread().getName(), list.get(i), time);
}

主线程(main)阻塞式的完成了所有的业务处理,费时费力,接下来加入多线程技术。

多线程技术加入

都使用多线程了,必须上线程池呀,不然线程没有管理,创建和销毁还是很费资源的。

项目使用的是SpringBoot工程搭建,所以使用的线程池技术就是:

初始化线程池

java 复制代码
@ConfigurationProperties(prefix = "task.pool")
@Component
@Data
public class TaskThreadPoolProperties {

    private Integer corePoolSize;

    private Integer maxPoolSize;

    private Integer keepAliveSeconds;

    private Integer queueCapacity;

}
yml 复制代码
task:
  pool:
    core-pool-size: 5
    max-pool-size: 10
    keep-alive-seconds: 300
    queue-capacity: 10
java 复制代码
@SpringBootConfiguration
@Slf4j
public class TaskThreadPoolConfig {

    @Autowired
    private TaskThreadPoolProperties taskThreadPoolProperties;

    @Bean(name = "threadPoolTaskExecutor", destroyMethod = "shutdown")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 设置核心线程数量
        executor.setCorePoolSize(taskThreadPoolProperties.getCorePoolSize());
        // 设置最大线程数量
        executor.setMaxPoolSize(taskThreadPoolProperties.getMaxPoolSize());
        // 设置队列容量
        executor.setQueueCapacity(taskThreadPoolProperties.getQueueCapacity());
        // 设置临时线程休闲存活时长
        executor.setKeepAliveSeconds(taskThreadPoolProperties.getKeepAliveSeconds());
        // 设置拒接策略
        // 默认 拒接 抛异常
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 拒接 丢弃任务
        // executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        // 拒接 丢弃队列最前面的任务
        // executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
        // 拒接 交给调用者处理
        // executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        // 初始化
        executor.initialize();
        log.info("任务线程池初始化完毕");
        return executor;
    }
    
}

以上操作,就准备好了一个简单的线程池了,需要解释记录的是:

  1. 核心处理线程个数:5
  2. 临时处理线程个数:5
  3. 任务等待队列长度:10
  4. 临时处理线程空闲销毁时间:300秒

以上的配置,只是为了测试,并不能作为一个工程的最终配置,线程的数量控制要考虑到功能的需要,是什么类型的操作,是I/O密集型,还是CPU密集型,硬件参数等来参考进行配置。
以上的配置,可以同时处理20个线程,超过20个线程,默认会拒绝任务,抛出异常,JUC包中带有的四种拒绝策略已经写在代码中了,后面我会记录自定义拒绝策略,来适应功能的数据完整性。

线程池执行业务

将业务代码包装成线程子类。

java 复制代码
@Slf4j
@Getter
public class TaskRunnable implements Runnable {

    private Map<String, Object> info;

    public TaskRunnable(Map<String, Object> info) {
        this.info = info;
    }

    @Override
    public void run() {
        try {
            // 模拟业务延迟时间
            int time = (new Random().nextInt(2) + 1) * 1000;
            Thread.sleep(time);
            log.info("线程:{},执行业务任务id:{},耗时:{} 毫秒完成", Thread.currentThread().getName(), info.get("id"), time);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

}

线程池执行业务线程。

java 复制代码
@Service
@Slf4j
public class TaskServiceImpl implements TaskService {

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Override
    public void task() throws InterruptedException {
        // 集合数据模拟
        List<Integer> list = new ArrayList<>();
        for (int i = 1; i <= 25; i++) {
            list.add(i);
        }

        // 循环执行业务
        for (int i = 0; i < list.size(); i++) {
            // 封装任务需要参数
            Map<String, Object> info = new HashMap<>();
            info.put("id", list.get(i));
            // 创建任务
            TaskRunnable taskRunnable = new TaskRunnable(info);
            // 执行任务
            threadPoolTaskExecutor.execute(taskRunnable);
        }

    }

}

这里是25个线程请求,必定会触发拒绝策略,默认抛出异常,拒绝执行。

不行,无论使用JUC带的4种的哪种,都会影响程序的执行速度,以及数据的丢失风险,所以加入自定义拒绝策略的处理。

自定义拒绝策略,主要实现 RejectedExecutionHandler 接口,重写rejectedExecution方法。

java 复制代码
@Component
@Slf4j
public class TaskRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 判断是哪个线程对象
        if (r instanceof TaskRunnable) {
            // 将当前执行的线程对象转换为自己的
            TaskRunnable runnable = (TaskRunnable) r;
            // 获取任务线程对象的数据
            Object id = runnable.getInfo().get("id");
            // 补救业务
            log.error("任务id:{} 被拒绝执行了,执行补救策略...", id);
        }
    }
}

配置自定义拒绝策略。

java 复制代码
@Autowired
private TaskRejectedExecutionHandler rejectedExecutionHandler;

// 配置自定义拒绝策略
executor.setRejectedExecutionHandler(rejectedExecutionHandler);

目前程序执行效果:

被拒绝的线程,会走到自定义拒绝策略的执行器中,可以进一步补救数据的丢失风险操作。

测试中遇到的问题

若使用线程池提交执行线程的方法是:submit

java 复制代码
threadPoolTaskExecutor.submit(taskRunnable);

刚刚自定义的拒绝策略中的线程对象就捕获不到了:

所以还是使用了 execute 方法来执行线程任务。

java 复制代码
threadPoolTaskExecutor.execute(taskRunnable);

最终笔记

  • 线程池的线程是赖加载模式,不会在初始化中创建好线程
  • 在核心线程数量未满时,新来的线程请求会创建新的核心线程来填满核心线程数量
  • 核心线程数量满时,新的线程请求会进入到任务队列中等待核心线程的处理
  • 核心线程数量和任务等待队列容量都满时,会创建临时线程
  • 核心线程数量,任务等待队列容器,临时线程数量都满时,会触发拒绝策略

以上是简单的线程使用过程,以后会遇到更高级的线程上的处理问题与处理方案的。

测试代码项目结构:

相关推荐
用户8356290780511 小时前
无需 Office:Python 批量转换 PPT 为图片
后端·python
啊哈灵机一动1 小时前
使用golang搭建一个nes 模拟器
后端
间彧2 小时前
SpringBoot + ShardingSphere 读写分离实战指南
后端
砍材农夫2 小时前
订单超时
后端
树獭叔叔3 小时前
06-大模型如何"学习":从梯度下降到AdamW优化器
后端·aigc·openai
得鹿3 小时前
MySQL基础架构与存储引擎、索引、事务、锁、日志
后端
程序员飞哥3 小时前
Block科技公司裁员四千人,竟然是因为 AI ?
人工智能·后端·程序员
JavaEdge在掘金4 小时前
Claude Code 直连 Ollama / LM Studio:本地、云端开源模型都能跑
后端
LSTM974 小时前
使用 Python 将 TXT 转换为 PDF (自动分页)
后端
于眠牧北4 小时前
Java开发学习提高效率的辅助软件和插件:一键生成接口文档,AI制作原型等
后端