🍉线程(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


BraveHope
7 声望0 粉丝