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<Void>(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会在下一小节介绍,这里我们只需要知道周期任务是如何重复执行即可。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容