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

功能模拟说明

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

最终笔记

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

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

测试代码项目结构:

相关推荐
2401_895521344 小时前
SpringBoot Maven快速上手
spring boot·后端·maven
disgare4 小时前
关于 spring 工程中添加 traceID 实践
java·后端·spring
ictI CABL4 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
小江的记录本6 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
yhole9 小时前
springboot三层架构详细讲解
spring boot·后端·架构
香香甜甜的辣椒炒肉9 小时前
Spring(1)基本概念+开发的基本步骤
java·后端·spring
白毛大侠10 小时前
Go Goroutine 与用户态是进程级
开发语言·后端·golang
ForteScarlet10 小时前
从 Kotlin 编译器 API 的变化开始: 2.3.20
android·开发语言·后端·ios·开源·kotlin
大阿明10 小时前
SpringBoot - Cookie & Session 用户登录及登录状态保持功能实现
java·spring boot·后端
Binary-Jeff10 小时前
Spring 创建 Bean 的关键流程
java·开发语言·前端·spring boot·后端·spring·学习方法