🍉线程(Thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每个线程可以并行执行不同的任务。本次主要分享多线程使用过程中的一些注意事项
业务背景:随着业务增长,用户数据也逐渐增多,历史业务逻辑是直接调用第三方服务的RPC获取用户数据,但针对大数据场景下,直接调用会对第三方服务造成压力,及接口响应性能产生影响,于是需要对用户维度的数据进行分库分表,并需要对依赖的第三方服务的历史数据进行迁移
迁移方案:使用多线程对大数据进行处理
执行任务的中断,执行速度控制
多线程执行任务的时候,要在执行过程中根据第三方服务的监控指标控制任务的执行速度,必要时中断任务的执行
🌴.volatile修饰的interrupt中断标志位,保证线程的可见性,中断标志采用apollo配置,运行时动态修改是否中断执行的任务
🌵.volatile修饰的sleepTime休眠时间,运行时动态修改线程执行任务的速度
apollo配置
@Value("${growth.report.migrate.interrupt:false}")
private volatile Boolean interrupt;
@Value("${growth.report.migrate.sleep:3}")
private volatile Integer sleepTime;
🍇线程池里的一个线程执行轮询任务,是否中断迁移任务执行
protected <T> void cancelMigrateTaskIfNecessary(List<CompletableFuture<T>> futures) {
ThreadPoolUtil.execute(() -> {
while (BooleanUtil.isTrue(interrupt)) {
log.warn("interrupt the growth report migrate task!");
futures.forEach(future -> future.cancel(true));
if (BooleanUtil.isTrue(interrupt)) {
break;
}
}
});
}
任务拆分,子任务partition
🍌迁移用户学习数据的时候,以班期维度拆分,每个班期一个线程处理,每个班期的任务里面又可以根据每个班期的用户进行分段,比如一个班期10000个用户,可以200个用户一个线程处理,然后处理这200个用户的线程里面再开启线程池1个用户1个线程处理,进行不断细分,充分利用线程的并行优势,减少IO阻塞
@XxlJob("growthReportDataMigrateTask")
public ReturnT<String> executeInternal(String param) {
log.info("task growthReportDataMigrateByClassTask start,classIds:{},classIds:{},param:{}", ids, classIds, param);
List<Integer> classIdList = Collections.emptyList();
if (StringUtils.isNotBlank(ids)) {
List<Long> courseIds = Arrays.stream(StringUtils.split(ids, CharPool.COMMA))
.map(Long::parseLong)
.collect(Collectors.toList());
classIdList = eduClassService.listClassIdsByCourseId(courseIds);
}
if (StringUtils.isNotBlank(classIds)) {
classIdList = Arrays.stream(StringUtils.split(classIds, CharPool.COMMA))
.map(Integer::parseInt)
.collect(Collectors.toList());
}
Assert.notEmpty(classIdList, "classIdList must not be empty!");
List<CompletableFuture<Void>> futures = classIdList.stream()
.map(classId -> CompletableFuture.runAsync(() ->
migrateReportData(classId), TtlExecutors.getTtlExecutorService(threadPoolExecutor)))
.collect(Collectors.toList());
cancelMigrateTaskIfNecessary(futures);
CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{})).join();
log.info("success migrate growthReportDataMigrateByClassTask!");
return ReturnT.SUCCESS;
}
private void migrateReportData(Integer classId) {
log.info("query user class Task start,classId:{}", classId);
List<EduUserClassDto> userClassDtos = userClassService.queryUserClassByClassId(classId);
log.info("classId {},query users:{}", classId, CollUtil.size(userClassDtos));
//分批处理班期的用户信息,根据用户数量进行分批处理,一个班期可能有10,000名用户,200多个课时
List<List<EduUserClassDto>> partitions = Lists.partition(userClassDtos, taskSize / 100);
List<CompletableFuture<Void>> futures = partitions.stream()
.map(partition -> CompletableFuture.runAsync(() ->
partitionMigrateUserClassReport(partition), ThreadPoolUtil.getPool()))
.collect(Collectors.toList());
cancelMigrateTaskIfNecessary(futures);
}
private void partitionMigrateUserClassReport(List<EduUserClassDto> userClassDtos) {
log.info("current users:{}", CollUtil.size(userClassDtos));
List<CompletableFuture<Void>> futures = userClassDtos.stream()
.map(userClassDto -> CompletableFuture.runAsync(() ->
migrateUserFinishedReport(userClassDto.getUserId(), userClassDto.getClassId()), ThreadPoolUtil.getPool()))
.collect(Collectors.toList());
cancelMigrateTaskIfNecessary(futures);
}
线程池隔离及动态线程池调整
线程池隔离
🍒任务拆分后的所有子任务放在一个线程池里面执行,那么就会造成一个线程执行任务时,需要等待拆分的子任务执行完才能进行后续的操作,而执行子任务的线程又要等待子任务线程执行完成,导致线程阻塞,所以需要对拆分出去的子任务放在另一个线程池里执行,避免任务的相互干扰
@Slf4j
@UtilityClass
public class ThreadPoolUtil {
/**
* 任务等待队列 容量
*/
private static final int TASK_QUEUE_SIZE = 100 * 1000;
/**
* 空闲线程存活时间 单位分钟
*/
private static final long KEEP_ALIVE_TIME = 10L;
/**
* 任务执行线程池
*/
private static final ExecutorService THREAD_POOL;
/**
* 任务执行线程池
*/
private static final ExecutorService THREAD_POOL_SUB;
/**
* 任务执行线程池
*/
private static final ThreadPoolExecutor threadPoolExecutor;
/**
* 任务执行线程池
*/
private static final ThreadPoolExecutor threadPoolExecutorSub;
static {
//io密集型,cpu核数2n+1
int corePoolNum = 2 * Runtime.getRuntime().availableProcessors() + 1;
int maximumPoolSize = 2 * corePoolNum;
threadPoolExecutor = new ThreadPoolExecutor(corePoolNum, maximumPoolSize, KEEP_ALIVE_TIME, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(TASK_QUEUE_SIZE),
new ThreadFactoryBuilder().setNameFormat("deal-third-service-main-task-%d").build(), (r, executor) -> {
if (!executor.isShutdown()) {
try {
//任务队列满了,阻塞执行
executor.getQueue().put(r);
} catch (InterruptedException e) {
log.warn("thread interrupted!", e);
Thread.currentThread().interrupt();
}
}
});
threadPoolExecutorSub = new ThreadPoolExecutor(corePoolNum, maximumPoolSize, KEEP_ALIVE_TIME, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(TASK_QUEUE_SIZE),
new ThreadFactoryBuilder().setNameFormat("deal-third-service-sub-task-%d").build(), (r, executor) -> {
if (!executor.isShutdown()) {
try {
//任务队列满了,阻塞执行
executor.getQueue().put(r);
} catch (InterruptedException e) {
log.warn("thread interrupted!", e);
Thread.currentThread().interrupt();
}
}
});
THREAD_POOL = TtlExecutors.getTtlExecutorService(threadPoolExecutor);
THREAD_POOL_SUB = TtlExecutors.getTtlExecutorService(threadPoolExecutorSub);
}
/**
* 提交任务到线程池
*
* @param task 任务
* @author <a>stephenXu</a>
*/
public static void execute(Runnable task) {
THREAD_POOL.execute(task);
}
/**
* 提交有返回结果的任务到线程池
*
* @param task 任务
* @author <a>stephenXu</a>
*/
public static <T> Future<T> submit(Callable<T> task) {
return THREAD_POOL.submit(task);
}
/**
* 获取线程池
*
* @author <a>stephenXu</a>
*/
public static ExecutorService getPool() {
return THREAD_POOL;
}
/**
* 获取辅助线程池
*
* @author <a>stephenXu</a>
*/
public static ExecutorService getSubPool() {
return THREAD_POOL_SUB;
}
/**
* 设置线程池核心线程数
*
* @author <a>stephenXu</a>
*/
public void setCoreSize(int coreSize) {
log.info("the pool core size:{}", coreSize);
threadPoolExecutor.setCorePoolSize(coreSize);
threadPoolExecutorSub.setCorePoolSize(coreSize);
}
/**
* 设置主线程池最大线程数
*
* @author <a>stephenXu</a>
*/
public void setMaxSizeMain(int maxSize) {
log.info("the main pool max size:{}", maxSize);
threadPoolExecutor.setMaximumPoolSize(maxSize);
}
/**
* 设置sub线程池最大线程数
*
* @author <a>stephenXu</a>
*/
public void setMaxSizeSub(int maxSize) {
log.info("the sub pool max size:{}", maxSize);
threadPoolExecutorSub.setMaximumPoolSize(maxSize);
}
}
动态线程池调整
🍊因为线程资源是有限的,需要在系统繁忙时动态增加线程池的线程,空闲时回收线程,另外如果线程过多,执行任务的时候对第三方服务会造成压力,所以通过配置动态调整线程池大小
@ApolloConfigChangeListener(value = "xxx.config.report", interestedKeyPrefixes = {"thread.pool."})
private void refreshThreadPool(ConfigChangeEvent configChangeEvent) {
ConfigChange coreSizeChange = configChangeEvent.getChange("thread.pool.core.size");
Optional.ofNullable(coreSizeChange).ifPresent(c -> ThreadPoolUtil.setCoreSize(Integer.parseInt(c.getNewValue())));
ConfigChange maxMainSizeChange = configChangeEvent.getChange("thread.pool.max.main.size");
Optional.ofNullable(maxMainSizeChange).ifPresent(c -> ThreadPoolUtil.setMaxSizeMain(Integer.parseInt(c.getNewValue())));
ConfigChange maxSubSizeChange = configChangeEvent.getChange("thread.pool.max.sub.size");
Optional.ofNullable(maxSubSizeChange).ifPresent(c -> ThreadPoolUtil.setMaxSizeSub(Integer.parseInt(c.getNewValue())));
}
总结
🍖多线程的恰当使用可以提高程序性能,缩短响应时间。毫不夸张的说多线程是大数据时代高效处理数据的必备利器,但在使用多线程的时候得考虑线程资源的占用,线程的中断,动态调整,对第三方服务的影响等等,避免使用多线程让自己的服务跑起来顺畅了,但因没有评估外部服务的瓶颈导致外部服务崩溃
觉得有收获的话可以关注微信公众号:程序员HopeX
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。