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

功能模拟说明

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

最终笔记

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

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

测试代码项目结构:

相关推荐
shark_chili7 分钟前
Netty-Reactor模型常见知识点小结
后端
brzhang10 分钟前
搞懂 Session、Cookie、JWT、SSO 到 OAuth 2.0:你的登录认证还好吗?
前端·后端·架构
重庆穿山甲21 分钟前
Spring Batch入门指南:让批处理变得简单
后端
brzhang35 分钟前
告别面条代码!用可视化编程 Flyde 给你的 Node.js/Web 应用逻辑解解耦
前端·后端·架构
这里有鱼汤37 分钟前
别怪 Python 慢,是你 import 的姿势不对!我亲测提速 3~5 倍
后端·python
小红帽的大灰狼40 分钟前
数据库建表时才知道我多菜
后端
冼紫菜1 小时前
探索微服务入口:Spring Cloud Gateway 实战指南
java·开发语言·后端·spring cloud·微服务·gateway
brzhang1 小时前
还在手撸线程?搞懂这 6 大多线程设计模式,并发编程不再难!
前端·后端·架构
这里有鱼汤1 小时前
都AI写代码了,我们还学Python干啥?我来告诉你为啥必须学!
后端
weixin_440597452 小时前
Spring Boot 中的条件注解
java·spring boot·后端