ScheduledThreadPoolExecutor

5、ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor可另外调度在给定延迟之后运行的命令,或定期执行。 ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。

ScheduledThreadPoolExecutor提交、执行任务的方法可分为延迟、周期两种。因前文对ThreadPoolExecutor已经做了较为详细的介绍,本篇

在正式开始分析ScheduledThreadPoolExecutor执行过程之前,先来看一下其构造函数:

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}

特别介绍一下其构造函数的目的是为了说明ScheduledThreadPoolExecutor使用了自己内部的任务队列DelayedWorkQueue,该类实现了BlockingQueue接口,其作用我们下文分析。

5.1、 延迟执行
// 示例
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(4);
executor.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("延迟10秒执行");
    }
}, 10, TimeUnit.SECONDS);

通过上面的配置,可以实现任务的延迟执行。

// 延迟执行任务
// command:任务;delay:延迟时间;unit:延迟时间单位
public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) {
    // 参数校验
    if (command == null || unit == null)
        throw new NullPointerException();
    // 将任务包装成RunnableScheduledFuture对象
    RunnableScheduledFuture t = decorateTask(command,
        new ScheduledFutureTask(command, null,triggerTime(delay, unit)));
    // 延迟执行                                  
    delayedExecute(t);
    return t;
}
// 延迟执行
private void delayedExecute(RunnableScheduledFuture task) {
    // 线程池非运行状态,根据回绝策略,回绝任务
    if (isShutdown())
        reject(task);
    else {
        // 将任务加入阻塞队列
        super.getQueue().add(task);
        // 二次校验,取消不符合执行条件的任务
        if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task))
            task.cancel(false);
        // 预启动
        else
            ensurePrestart();
    }
}

任务加入阻塞队列后,线程池的状态可能发生了变化,所以要进行二次校验。这里涉及到一个知识点,先来看ScheduledThreadPoolExecutor中的两个变量:

// 线程池关闭后(SHUTDOWN)是否继续执行周期性任务。默认false
private volatile boolean continueExistingPeriodicTasksAfterShutdown;
// 线程池关闭后(SHUTDOWN)是否继续执行延迟性任务。默认true
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;

当线程处于非运行状态时,根据任务性质(延迟、周期)并结合这两个变量值以确定任务是否继续执行。

// 预启动
void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
        addWorker(null, true);
    else if (wc == 0)
        addWorker(null, false);
}
5.2、获取任务

因为延迟、周期任务需要延期执行,所以addWorker()方法并没有传入具体的任务,而是从任务队列中获取。这里先要了解一下Leader-Follower模式。

[图片上传失败...(image-faba54-1605494023030)]

上图就是L/F多线程模型的状态变迁图,共6个关键点:

  1. 线程有3种状态:领导leading,处理processing,追随following。
  2. 假设共N个线程,其中只有1个leading线程(等待任务),x个processing线程(处理),余下有N-1-x个following线程(空闲)。
  3. 有一把锁,谁抢到就是leading。
  4. 事件/任务来到时,leading线程会对其进行处理,从而转化为processing状态,处理完成之后,又转变为following。
  5. 丢失leading后,following会尝试抢锁,抢到则变为leading,否则保持following。
  6. following不干事,就是抢锁,力图成为leading。

以上Leader-Follower内容摘抄自W3CSchool

了解了Leader-Follower线程模型后,再来看take()方法就事半功倍。take()方法的大体思想可以看做是Leader-Follower线程模型的一种变体。使用这种模式可以有效的减少不必要的等待时间。当一个线程成为leader时,它只等待下一个延迟过去,而其他线程则无限期地等待。在从take()或poll()返回之前,leader线程必须给其他线程发送信号,除非在此期间有其他线程成为leader。每当队列的头部被一个过期时间较早的任务替换时,leader字段就会被重置为null,一些等待的线程(不一定是当前的leader)就会被发送信号。

public RunnableScheduledFuture take() throws InterruptedException {
    // 加锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 自旋
        for (;;) {
            RunnableScheduledFuture first = queue[0];
            // 任务队列为空,等待,直至有任务加入
            if (first == null)
                available.await();
            else {
                long delay = first.getDelay(NANOSECONDS);
                // 下一次任务执行间隔时间小于等于零,表明该任务可以被执行,将任务移出队列并返回
                if (delay <= 0)
                    return finishPoll(first);
                first = null; // don't retain ref while waiting
                // 若已经有线程成为leader,则其他线程等待leader线程执行完毕
                if (leader != null)
                    available.await();
                else {
                    // 无线程成为leader,则将当前线程置为leader,并等待延迟时间过去
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay);
                    } finally {
                        // 释放leader
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // leader已释放、且队列中还有待执行任务,通知其他线程
        // 其他线程将被唤醒,并争抢leader
        if (leader == null && queue[0] != null)
            available.signal();
        // 释放锁
        lock.unlock();
    }
}

take()方法虽然思想简单,对于线程基础薄弱的同学,理解代码执行过程较为困难。take方法虽然使用ReentrantLock对方法加锁,但是在leader线程等待延迟执行的时候,通过调用Condition的awaitNanos()方法释放同步状态,使得其它线程依然可以获取锁。而其它线程进入take()方法后,发现leader线程不为空,则调用Condition的await()方法无限期等待,直至leader线程发起通知。

5.3、执行任务
public void run() {
    boolean periodic = isPeriodic();
    if (!canRunInCurrentRunState(periodic))
        cancel(false);
    // 执行延迟任务
    else if (!periodic)
        ScheduledFutureTask.super.run();
    // 执行周期任务
    else if (ScheduledFutureTask.super.runAndReset()) {
        // 设置下次执行任务时间
        setNextRunTime();
        // 重复执行周期任务
        reExecutePeriodic(outerTask);
    }
}

前文已经介绍过,ScheduledThreadPoolExecutor会对任务做一层包装,包装底层使用的是FutureTask。FutureTask会在下一小节介绍,这里我们只需要知道周期任务是如何重复执行即可。

你可能感兴趣的:(ScheduledThreadPoolExecutor)