定时任务线程池源码解读(读这一篇就够了)

一、引言

        在《并发编程的艺术》一书中对ScheduledThreadPoolExecutor的介绍还是基于jdk1.6版本,而在jdk1.7之后ScheduledThreadPoolExecutor发生了较大的变化,其使用的阻塞队列由之前的DelayQueue变成了DelayedWorkQueue。DelayedWorkQueue是DelayQueue(take进行delay判断)和PriorityQueue(堆结构)的结合。

        ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。

        ScheduledThreadPoolExecutor是一个用于执行定时任务或延时任务的线程池,提交到该线程池中的任务会等到执行时间到了才会被执行。与前面所讲的ThreadPoolExecutor不同,提交到ThreadPoolExecutor中的任务,只要ThreadPoolExecutor中有空闲线程任务会被从任务队列马上取出(或者未达到核心线程数),就会被马上执行,如果ThreadPoolExecutor中没有空闲线程,则任务会被暂存在任务队列。而提交到ScheduledThreadPoolExecutor中的任务,不管此时ScheduledThreadPoolExecutor有没有空闲线程,任务都会被放入到队列里去,等待任务执行时间到期时被线程从队列中取出并执行。

        假如在看ScheduleThreadPoolExecutor的源码之前,我们凭自己思路设计一个定时任务线程池,需要怎么设计呢?

        普通线程池的运行机制是从任务队列队首位置取出任务后,需要立马执行,但是定时任务线程池不一样,每个任务都有自己的定时执行时间,或者延时运行时间,任务队列,队首的任务也不是要立马执行。

        因为需要定期执行,每个定时任务其实是重复执行,所以可以看做定时任务线程池一旦初始化建立,并启动后,队列里面的任务是固定的。每个任务都安排一个线程沉睡到指定执行的时间点,然后唤醒该线程并执行,貌似是可以的,但是假如这些任务的执行时间是没有重合,前后的时间间隔也比较大,明明一个线程就可以搞定,却新建了很多线程,占用内存,浪费资源,此时,阻塞任务队列也成为摆设,毫无意义。所以这种思路不可取。

        既然每个任务都有一个线程蹲守watch方案不可取,那就只蹲守watch一个任务,那蹲守哪个任务呢?肯定是延时执行时间最临近当前时间的任务了,怎么知道哪个任务的延时执行时间是最临近的呢,必须有线程在添加新任务时候,给阻塞任务队列里面的任务,按照延时时间排排序,这就是涉及到适合延时任务队列的最优的排序的算法了。肯定还需要一个哨兵线程来蹲守即将到期的任务。

        如果线程池里面的任务执行时间有重合,任务过期,线程数不够,发生拥挤又怎么办呢?该怎么优化?

JAVA的源码又是怎么做的呢?带着这些问题,去源码中寻找答案。

二、源码解析(JDK1.8)

        2.1、构造方法

虽然ScheduledThreadPoolExecutor源码将构造方法放到后面展开,将它的专属任务类放到最前面,但是既然引言对其与父类ThreadPoolExcutor做了对比,这里还是先讲一下它设定的几个构造方法:

/**
 * 默认线程工厂、拒绝策略
 */
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

/**
 * 带线程工厂,可以给线程自定义名字
 */
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory);
}

/**
* 自定义一个拒绝策略,如果线程池被shutDown后会用得着
*/
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   RejectedExecutionHandler handler) {
    // 因为是无界队列,所以最大线程数是没有用的,如果核心线程数为0,那么就会只有一个工作线程
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), handler);
}

/**
 * 几个构造方法生成的线程池都是最大线程数和无界队列,如果有程序一直往线程池提交延时任务,也会造成OOM的风险
 * 所以使用该线程池一定有把握,延时任务不会瞬时超大批量的添加,当然实际使用中,该场景也比较少。
 * 实在无法避免此风险,就自己另外定义一个子类,做扩展,并将队列和最大线程数设置成有界
 * 并自定义合适拒绝策略
 */
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory,
                                   RejectedExecutionHandler handler) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue(), threadFactory, handler);
}

ThreadPoolExcutor的父类AbstractExecutorService,里面的submit()方法:

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

        2.1.1、可接受的任务task:FutureTask

        虽然提交的都是Runnable或者Callable实现类,但其实都被包装成FutureTask,也就是RunnableFuture的实现类。FutureTask可以看做是Callable的包装类,内部有Callable 和保存其结果的 outcome ,同时它实现RunnableFuture也就是同时实现了 Runnable 和 Future ,有run() 和 get() 方法,run()调用的Callable的call(),将结果存在outcome,它构造方法可以接受Callable类型的参数,也可以接受Runnable类型的参数,但如果是Runnble,可不是直接用来实现Runnable接口用的,而是将Runnbale适配成Callable(Callable调用的是Runnbale的run(),FutureTask的run() 又是再调用call() 饶了一圈)。不管怎么样,它既然实现Runnable,也就是可以被装配到线程里异步运行。由此可见,它设计的思路,主要是融合Callable 和 Future,同时为了可以被提交到线程池,装配给线程执行,而实现Runable。说白了,它是整合了 Callable、Future、Runnable的产物。

        它存在的意义还是实现可提交到线程异步执行,并能返回结果的任务类型(当然,如果调用get()获取返回结果,就是同步获取结果了),需要的话,可以读一下FutureTask的源码。既然ThreadPoolExcutor可接受的任务的定义这么重要,那么ScheduledThreadPoolExecutor可接受的延时任务,当然也重要,所以在ScheduledThreadPoolExecutor源码中,ScheduledFutureTask比较靠前。

2.2、ScheduledFutureTask

        2.2.1、ScheduledFutureTask的UML图

2.2.2、ScheduledFutureTask源码

private class ScheduledFutureTask<V>
        extends FutureTask<V> implements RunnableScheduledFuture<V> {

    /** 每个延时任务都有一个次序号,防止在在延时队列中排序的时候,
     * 两个任务的延时时间一样,出现排序冲突,则通过次序号来决定
     * 虽然任务是周期执行的,但是一旦任务被提交,次序号就定了,重复放入队列,并不会改变次序号
     */
    private final long sequenceNumber;

    /** 任务执行的时刻,在执行完后重复放入队列的时候,会被更新 */
    private long time;

    /**
     * 循环执行的间隔时间
     */
    private final long period;

    /** The actual task to be re-enqueued by reExecutePeriodic */
    RunnableScheduledFuture<V> outerTask = this;

    /**
     * 延时队列小顶堆中的下标
     */
    int heapIndex;

    /**
     * 周期为0,非周期性任务,将runnable装配成callable再
     */
    ScheduledFutureTask(Runnable r, V result, long ns) {
        super(r, result);
        this.time = ns;
        this.period = 0;
        this.sequenceNumber = sequencer.getAndIncrement();
    }

    /**
     * Creates a periodic action with given nano time and period.
     */
    ScheduledFutureTask(Runnable r, V result, long ns, long period) {
        super(r, result);
        this.time = ns;
        this.period = period;
        this.sequenceNumber = sequencer.getAndIncrement();
    }

    /**
     * 非周期性任务callable
     */
    ScheduledFutureTask(Callable<V> callable, long ns) {
        super(callable);
        this.time = ns;
        this.period = 0;
        this.sequenceNumber = sequencer.getAndIncrement();
    }

    /**
     * 延时时间:RunnableScheduledFuture接口继承RunnableFuture、 ScheduleFuture, ScheduleFuture继承Delayed, Future
     * Delaye(extends Comparable)接口有getDelay,延时队列的take() poll(long timeout, TimeUnit unit) 会用到
     */
    public long getDelay(TimeUnit unit) {
        return unit.convert(time - now(), NANOSECONDS);
    }

    public int compareTo(Delayed other) {
        if (other == this) // compare zero if same object
            return 0;
        if (other instanceof ScheduledFutureTask) {
            ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
            long diff = time - x.time;
            if (diff < 0)
                return -1;
            else if (diff > 0)
                return 1;
            // 或许延时时间有相等的,但序号是不可能相等的
            else if (sequenceNumber < x.sequenceNumber)
                return -1;
            else
                return 1;
        }
        long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
        return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
    }

    /**
     * 源自RunnableScheduledFuture,是否周期性执行
     */
    public boolean isPeriodic() {
        return period != 0;
    }

    /**
     * 下次执行时间
     */
    private void setNextRunTime() {
        long p = period;
        if (p > 0)
            time += p;
        else
        // 后面解析触发器
            time = triggerTime(-p);
    }

    public boolean cancel(boolean mayInterruptIfRunning) {
        boolean cancelled = super.cancel(mayInterruptIfRunning);
        if (cancelled && removeOnCancel && heapIndex >= 0)
            remove(this);
        return cancelled;
    }

    /**
     * 该任务task的run方法: 线程池 Worker中的线程start()执行,Worker -> worker.run() -> runWork()执行
     * while (task != null || (task = getTask()) != null) {task.run}
     */
    public void run() {
        boolean periodic = isPeriodic();
        // 是否还能运行,这个方法很重要,紧接着ScheduledThreadPoolExecutor内部源码解析会做详细解析
        if (!canRunInCurrentRunState(periodic))
            cancel(false);
        else if (!periodic)
            ScheduledFutureTask.super.run();
            // cancle的任务就算被放进队列,这一步是不会执行的
        else if (ScheduledFutureTask.super.runAndReset()) {
            // 重新放入队列的时候,设置下次的执行的时间,time 在getDelay会用到,delay在 
            // 延时队列的take -> available.awaitNanos(delay);
            setNextRunTime();
            // 本次执行完,重新放入队列:可以理解为周期性反复执行
            reExecutePeriodic(outerTask);
        }
    }
}

2.3、ScheduledThreadPoolExecutor的源码解析


                
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

砥砺code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值