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

功能模拟说明

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

最终笔记

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

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

测试代码项目结构:

相关推荐
AskHarries1 小时前
Java字节码增强库ByteBuddy
java·后端
佳佳_1 小时前
Spring Boot 应用启动时打印配置类信息
spring boot·后端
许野平2 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
BiteCode_咬一口代码3 小时前
信息泄露!默认密码的危害,记一次网络安全研究
后端
齐 飞4 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod4 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
码农派大星。5 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man6 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*6 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu6 小时前
Go语言结构体、方法与接口
开发语言·后端·golang