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

功能模拟说明

一个很长的集合,集合中存储的是要远程调用相应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);

最终笔记

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

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

测试代码项目结构:

相关推荐
仙俊红17 分钟前
Spring Cloud 核心组件部署方式速查表
后端·spring·spring cloud
码农幻想梦40 分钟前
实验九 Restful和ajax实现
后端·ajax·restful
今天多喝热水1 小时前
SpEL(Spring Expression Language) 表达式
java·后端·spring
码农水水1 小时前
浅谈 MySQL InnoDB 的内存组件
java·开发语言·数据库·后端·mysql·面试
独自破碎E1 小时前
Spring Boot的多环境配置
java·spring boot·后端
Edward-tan2 小时前
【玩转全栈】----Django模板语法、请求与响应
后端·python·django
猫头鹰源码(同名B站)2 小时前
基于django+vue的时尚穿搭社区(商城)(前后端分离)
前端·javascript·vue.js·后端·python·django
Watermelo6172 小时前
随机扣款实现赛博共产主义,《明日方舟:终末地》公测支付事故复盘
数据库·后端·游戏程序·技术美术·用户体验·游戏策划·游戏美术
观音山保我别报错2 小时前
Spring Boot 项目学习内容详解(一)
spring boot·后端·学习
哪里不会点哪里.2 小时前
Spring Boot 启动原理深度解析
java·spring boot·后端