功能模拟说明
一个很长的集合,集合中存储的是要远程调用相应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;
}
}
以上操作,就准备好了一个简单的线程池了,需要解释记录的是:
- 核心处理线程个数:5
- 临时处理线程个数:5
- 任务等待队列长度:10
- 临时处理线程空闲销毁时间: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);
最终笔记
- 线程池的线程是赖加载模式,不会在初始化中创建好线程
- 在核心线程数量未满时,新来的线程请求会创建新的核心线程来填满核心线程数量
- 核心线程数量满时,新的线程请求会进入到任务队列中等待核心线程的处理
- 核心线程数量和任务等待队列容量都满时,会创建临时线程
- 核心线程数量,任务等待队列容器,临时线程数量都满时,会触发拒绝策略
以上是简单的线程使用过程,以后会遇到更高级的线程上的处理问题与处理方案的。
测试代码项目结构: