XXL-Job源码阅读(一)JobTriggerPoolHelper

本文源码阅读的主角是JobTriggerPoolHelper,作业触发线程池助手,通过阅读此部分源码,我们可以学习到:

  1. 线程池隔离技术
  2. 多线程知识

快慢线程池

JobTriggerPoolHelper类中,有这么两个变量,定义了两个线程池

// fast/slow thread pool
private ThreadPoolExecutor fastTriggerPool = null;
private ThreadPoolExecutor slowTriggerPool = null;
复制代码

两个线程池在启动的时候会被初始化

public void start() {
    fastTriggerPool = new ThreadPoolExecutor(
        10,
        XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(), // 最大线程数,最小为200
        60L,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1000), // 队列数1000
        r -> new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()));

    slowTriggerPool = new ThreadPoolExecutor(
        10,
        XxlJobAdminConfig.getAdminConfig().getTriggerPoolSlowMax(), // 最大线程数,最小为100
        60L,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(2000), // 队列数2000
        r -> new Thread(r, "xxl-job, admin JobTriggerPoolHelper-slowTriggerPool-" + r.hashCode()));
}
复制代码

通过源码,我们发现,快慢线程池除了最大线程数和队列数不一致外,其他是相同的,这一开始让我很困惑,难道修改最大线程数和任务队列数,可以让线程跑的更快吗?

查阅其他资料后,恍然大悟,还是功力不够。

实际上不是线程池本身的快慢,而是线程池里面的任务执行有快慢一说,只有执行快的,才有资格加入fastTriggerPool,执行慢的就只能被打入slowTriggerPool

总的来讲,使用线程池隔离技术,将快任务和慢任务分开执行,可以避免某些任务执行过慢,从而拖累原本执行快的任务,优化整体的任务执行性能。

这一招在Hystrix里面似乎见过。

慢任务的定义

既然要将慢任务打入冷宫,那么我们怎么定义这个任务执行的慢呢?

XXL-Job是这么定义的:任务执行时间大于500ms,那么认为是慢任务(超时),此时记为一次;如果1分钟内超时次数大于10次,就会被打入冷宫。

具体做法:

  1. 定义变量minTim,记录当前任务执行结束时间和60000(1分钟的毫秒值)的比值
private volatile long minTim = System.currentTimeMillis() / 60000;     // ms > min
复制代码
  1. 定义变量jobTimeoutCountMap,记录每个任务的执行超时次数,该变量间隔大于等于1分钟会重置
// 此map用来记录任务超时次数,不过实际上并不是超时,只是执行时间大于500ms,应该叫做执行慢的次数才对
private volatile ConcurrentMap<Integer, AtomicInteger> jobTimeoutCountMap = new ConcurrentHashMap<>();

// check timeout-count-map
long minTim_now = System.currentTimeMillis() / 60000;
// 当前时间戳和60000相除的结果,如果和既定变量不一致,那么肯定已经过去1分钟甚至更久,那么清空jobTimeoutCountMap
if (minTim != minTim_now) {
    minTim = minTim_now;
    jobTimeoutCountMap.clear();
}
复制代码
  1. 在每次任务执行结束后,判断任务执行时长是否大于500ms,如果是,那么记为超时一次
// incr timeout-count-map
long cost = System.currentTimeMillis() - start;
// 如果任务执行时间大于500ms,那么记为超时一次
if (cost > 500) {       // ob-timeout threshold 500ms
    AtomicInteger timeoutCount = jobTimeoutCountMap.putIfAbsent(jobId, new AtomicInteger(1));
    if (timeoutCount != null) {
        timeoutCount.incrementAndGet();
    }
}
复制代码
  1. 在每次任务调度前,选择合适的线程池来执行
// choose thread pool
ThreadPoolExecutor triggerPool_ = fastTriggerPool;
AtomicInteger jobTimeoutCount = jobTimeoutCountMap.get(jobId);
// 作业-1分钟内超时10次,将会使用慢线程池来执行
if (jobTimeoutCount != null && jobTimeoutCount.get() > 10) {      // job-timeout 10 times in 1 min
    triggerPool_ = slowTriggerPool;
}
复制代码

看到这里,感觉这个慢任务线程池我司似乎用不上,因为定时任务都是批处理任务,耗时都挺长的,永远也达不成1分钟内超时10次的硬指标,因为任务本身耗时就大于1分钟。

其他多线程知识

volatile的使用

开始前,先复习下volatile的语义:所有线程都能看到共享内存的最新状态。也就是说,线程总是能拿到该变量最新的值。

minTimjobTimeoutCountMap都使用了volatile进行修饰。

这里要注意一下,任务调度的部分,是无锁并发,没有使用任何锁。

// trigger
triggerPool_.execute(() -> {
    long start = System.currentTimeMillis();
    try {
        // do trigger
        XxlJobTrigger.trigger(jobId, triggerType, failRetryCount, executorShardingParam, executorParam, addressList);
    } catch (Exception e) {
        LOGGER.error(e.getMessage(), e);
    } finally {
        // check timeout-count-map
        ...

        // incr timeout-count-map
        ...
    }
});
复制代码

minTim好理解,因为要和当前计算的值进行比较,所以使用volatile,取最新值进行比较就行。

但是jobTimeoutCountMap的用法存疑,因为本身类型就是ConcurrentMap,读写是原子性的,而且volatile修饰的是容器类,实际上容器类我们可以使用final来修饰更好一点,因为容器本身不需要改变,改变的只是其中的元素而已。

结合Do we need to make ConcurrentHashMap volatile?,我感觉这边应该使用final修饰更好一点,有没有懂哥赐教下的,欢迎评论留言。

线程池初始化

我们来看一下快线程池的初始化

fastTriggerPool = new ThreadPoolExecutor(
    10,
    XxlJobAdminConfig.getAdminConfig().getTriggerPoolFastMax(), // 最大线程数,最小为200
    60L,
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000), // 队列数1000
    r -> new Thread(r, "xxl-job, admin JobTriggerPoolHelper-fastTriggerPool-" + r.hashCode()));
复制代码
  1. 核心线程数:10
  2. 最大线程数:可配置,如果配置数小于200,那么设置为200,意味着最小是200
  3. 线程空闲时间:60秒
  4. 任务队列:容量为1000的有界阻塞队列
  5. 线程工厂:定义了下线程名称

回顾一下线程池的知识,什么情况下会到达最大线程数呢?

答:当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程并执行。

也就是说,10个任务在运行,1000个任务待运行,然后此时添加任务,就会创建线程,并且短时间内疯狂的调度200个任务,才能到达最大线程数。

此时应该是极端情况了,一般是不会积压这么多待运行任务的。

算是巩固了下对线程池的理解,在实际业务场景遇到也可作为参考。

附录

猜你喜欢

转载自juejin.im/post/7190395870926340152