自jdk1.5开始,Java开始提供ScheduledThreadPoolExecutor类来支持周期性任务的调度,在这之前,这些工作需要依靠Timer/TimerTask或者其它第三方工具来完成。但Timer有着不少缺陷,如Timer是单线程模式,调度多个周期性任务时,如果某个任务耗时较久就会影响其它任务的调度;如果某个任务出现异常而没有被catch则可能导致唯一的线程死掉而所有任务都不会再被调度。ScheduledThreadPoolExecutor解决了很多Timer存在的缺陷。

先来看看ScheduledThreadPoolExecutor的实现模型,它通过继承ThreadPoolExecutor来重用线程池的功能,里面做了几件事情:

  • 为线程池设置了一个DelayedWorkQueue,该queue同时具有PriorityQueue(优先级大的元素会放到队首)和DelayQueue(如果队列里第一个元素的getDelay返回值大于0,则take调用会阻塞)的功能
  • 将传入的任务封装成ScheduledFutureTask,这个类有两个特点,实现了java.lang.Comparable和java.util.concurrent.Delayed接口,也就是说里面有两个重要的方法:compareTo和getDelay。ScheduledFutureTask里面存储了该任务距离下次调度还需要的时间(使用的是基于System#nanoTime实现的相对时间,不会因为系统时间改变而改变,如距离下次执行还有10秒,不会因为将系统时间调前6秒而变成4秒后执行)。getDelay方法就是返回当前时间(运行getDelay的这个时刻)距离下次调用之间的时间差;compareTo用于比较两个任务的优先关系,距离下次调度间隔较短的优先级高。那么,当有任务丢进上面说到的DelayedWorkQueue时,因为它有DelayQueue(DelayQueue的内部使用PriorityQueue来实现的)的功能,所以新的任务会与队列中已经存在的任务进行排序,距离下次调度间隔短的任务排在前面,也就是说这个队列并不是先进先出的;另外,在调用DelayedWorkQueue的take方法的时候,如果没有元素,会阻塞,如果有元素而第一个元素的getDelay返回值大于0(前面说过已经排好序了,第一个元素的getDelay不会大于后面元素的getDelay返回值),也会一直阻塞。
  • ScheduledFutureTask提供了一个run的实现,线程池执行的就是这个run方法。看看run的源码(本文的代码取自hotspot1.5.0_22,jdk后续版本的代码可能已经不一样了,如jdk1.7中使用了自己实现的DelayedWorkQueue,而不再使用PriorityQueue作为存储,不过从外面看它们的行为还是一样的,所以并不影响对ScheduledThreadPoolExecutor调度机制的理解):

     

    public void run() {
        if (isPeriodic())
            runPeriodic();
        else
            ScheduledFutureTask.super.run();
    }

    如果不是周期性任务就直接执行任务(也就是else部分),这个主要是用于实现ScheduledThreadPoolExecutor#schedule(Callable callable, long delay, TimeUnit unit)和ScheduledThreadPoolExecutor#schedule(Runnable command, long delay, TimeUnit unit),后面会讲到它们的实现,这里先关注周期任务的执行方式。周期性任务执行的是runPeriodic(),看下它的实现:

    private void runPeriodic() {
        boolean ok = ScheduledFutureTask.super.runAndReset();
        boolean down = isShutdown();
        // Reschedule if not cancelled and not shutdown or policy allows
        if (ok && (!down ||
                   (getContinueExistingPeriodicTasksAfterShutdownPolicy() &&
                    !isTerminating()))) {
            long p = period;
            if (p > 0)
                time += p;
            else
                time = triggerTime(-p);
            ScheduledThreadPoolExecutor.super.getQueue().add(this);
        }
        // This might have been the final executed delayed
        // task.  Wake up threads to check.
        else if (down)
            interruptIdleWorkers();
    }

    这里可以看到,先执行了任务本身(ScheduledFutureTask.super.runAndReset),这个调用有一个返回值,来看下它的实现:

    protected boolean runAndReset() {
        return sync.innerRunAndReset();
    }

    跟进去看下innerRunAndReset():

    boolean innerRunAndReset() {
        if (!compareAndSetState(0, RUNNING))
            return false;
        try {
            runner = Thread.currentThread();
            callable.call(); // don't set result
            runner = null;
            return compareAndSetState(RUNNING, 0);
        } catch(Throwable ex) {
            innerSetException(ex);
            return false;
        }
    }

    可以发现,这里需要关注的是第三个return,也就是如果任务执行出现了异常,会被catch且返回false.

    继续看runPeriodic()方法,if里面,如果刚才任务执行的返回值是true且线程池还在运行就在if块中的操作,如果线程池被关闭了就做else if里的操作。也就是说,如果之前的任务执行出现的异常返回了false,那么if里以及else if里的代码都不会执行了,那有什么影响?接下来看看if里做了什么。

    if里的代码很简单,分为两部分,一是计算这个任务下次调度的间隔,二是将任务重新放回队列中。回到出现异常的情况,如果刚才的任务执行出现了异常,就不会将任务再放回队列中,换而言之,也就是这个任务再也得不到调度了!但是,这并不影响其它周期任务的调度。

综上,可以看到,ScheduledThreadPoolExecutor执行周期性任务的模型就是:调度一次任务,计算并设置该任务下次间隔,将任务放回队列中供线程池执行。这里的队列起了很大的作用,且有一些特点:距离下次调度间隔短的任务总是在队首,队首的任务若距离下次调度的间隔时间大于0就无法从该队列的take()方法中拿到任务。

接下来看看ScheduledThreadPoolExecutor#schedule(Callable callable, long delay, TimeUnit unit)和ScheduledThreadPoolExecutor#schedule(Runnable command, long delay, TimeUnit unit)这两个非周期性任务的实现方式,先看看它们的源码:

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    ScheduledFutureTask<?> t =
        new ScheduledFutureTask<Boolean>(command, null, triggerTime(delay, unit));
    delayedExecute(t);
    return t;
}
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                       long delay,
                                       TimeUnit unit) {
    if (callable == null || unit == null)
        throw new NullPointerException();
    ScheduledFutureTask<V> t =
        new ScheduledFutureTask<V>(callable, triggerTime(delay, unit));
    delayedExecute(t);
    return t;
}
private void delayedExecute(Runnable command) {
    if (isShutdown()) {
        reject(command);
        return;
    }
    // Prestart a thread if necessary. We cannot prestart it
    // running the task because the task (probably) shouldn't be
    // run yet, so thread will just idle until delay elapses.
    if (getPoolSize() < getCorePoolSize())
        prestartCoreThread();
 
    super.getQueue().add(command);
}

实现方式也很简单,在创建ScheduledThreadPoolExecutor内部任务(即ScheduledFutureTask)的时候就将调度间隔计算并设置好,如果当前线程数小于设置的核心线程数,就启动一个线程(可能是线程池刚启动里面还没有线程,也可能是里面的线程执行任务时挂掉了。如果线程池中的线程挂掉了而又没有调用这些schedule方法谁去补充挂掉的线程?不用担心,线程池自己会处理的)去监听队列里的任务,然后将任务放到队列里,在任务执行间隔不大于0的时候,线程就可以拿到这个任务并执行。

周期性任务的入口(ScheduledThreadPoolExecutor#scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)和ScheduledThreadPoolExecutor#scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit))与非周期性任务是类似的,它们处理方式不同的地方在于前文说到的ScheduledFutureTask#run()中。

posted @ 2018-04-26 13:20 wwfy 阅读(1572) 评论(0) 推荐(0) 编辑
摘要: ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类; JDK api里是这么说的: ThreadPoolExecutor,它可另行安排在给定的延迟后运行命令,或者定期执行命令。需要多个辅助线程时,或者要求 ThreadPoolExecutor 具有额外 阅读全文
posted @ 2018-04-24 19:05 wwfy 阅读(996) 评论(0) 推荐(0) 编辑
摘要: 导入了别人的项目,各种jar包都放好后,path也都build好了,项目也能正常启动,但是就是项目名有红叉,这是为什么呢? 网上有人说Java build path中的jar包missing了,这是一种常见的错误。 其实还有一种错误是:当我们在创建项目时候,可以独立定义一个模板供我们使用,在里面定义 阅读全文
posted @ 2018-04-22 23:33 wwfy 阅读(2934) 评论(0) 推荐(0) 编辑
摘要: 引言 今天在创建java项目的时候遇到了很多的错误,在解决的过程中遇到了一些根本不知道什么作用的文件,然后按照网上的一些做法可以将问题解决,但是这也说明我们在学习的时候很多基础和细节的地方是我们薄弱的地方,我们总是对于经常写代码的那几个文件比较熟悉,这样一旦报错我们就无从下手啊。 下面介绍几个文件。 阅读全文
posted @ 2018-04-22 23:08 wwfy 阅读(960) 评论(0) 推荐(0) 编辑
摘要: 今天发现早年在大象笔记中写的一篇笔记,之前放在ijavaboy上的,现在它已经访问不了了。前几天又有同事在讨论这个问题。这里拿来分享一下。 在web应用开发或者游戏服务器开发的过程中,我们时时刻刻都在使用热部署。热部署的目的很简单,就是为了节省应用开发和发布的时间。比如,我们在使用Tomcat或者J 阅读全文
posted @ 2018-04-22 22:52 wwfy 阅读(1141) 评论(0) 推荐(0) 编辑
摘要: 博客分类: java基础 博客分类: java基础 myeclipse中的classpath是一个很重要的问题 myeclipse的在查找的时候都是按照其查找,而且myeclipse有一个专门的文件来保存classpath的信息,这也是别人的项目拷贝的时候需要的一个重要文件,不然这个项目要导入到自己 阅读全文
posted @ 2018-04-22 22:48 wwfy 阅读(1207) 评论(0) 推荐(0) 编辑
摘要: Linux磁盘概念及其管理工具fdisk:http://www.linuxidc.com/Linux/2016-08/134664.htm 一、 进入linux虚拟机 右键 open in terminal su 输入密码切换为root用户 fdisk -l 查看磁盘情况 我的目前只有sda一个磁盘 阅读全文
posted @ 2018-04-17 23:15 wwfy 阅读(14325) 评论(0) 推荐(2) 编辑
摘要: 一、查看新磁盘[root@desktop61 Desktop]# fdisk -cul /dev/sdcDisk /dev/sdc: 21.5 GB, 21474836480 bytes255 heads, 63 sectors/track, 2610 cylinders, total 419430 阅读全文
posted @ 2018-04-17 23:09 wwfy 阅读(6385) 评论(0) 推荐(0) 编辑
摘要: / 根目录 / 根目录 唯一必须挂载的目录。不要有任何的犹豫,选一个分区,挂载它!(在绝大多数情况下,有10G的容量应该是够用了。当然了,很多东西都是多多益善的) /boot /boot 它包含了操作系统的内核和在启动系统过程中所要用到的文件,建这个分区是有必要的,启动分区,即使主要的根分区出现了问 阅读全文
posted @ 2018-04-17 22:59 wwfy 阅读(1154) 评论(0) 推荐(0) 编辑
摘要: 基本概念 硬盘分区有三种,主磁盘分区、扩展磁盘分区、逻辑分区。 一个硬盘主分区至少有1个,最多4个,扩展分区可以没有,最多1个。且主分区+扩展分区总共不能超过4个。逻辑分区可以有若干个。 在windows下激活的主分区是硬盘的启动分区,他是独立的,也是硬盘的第一个分区,正常分的话就是C区。 在lin 阅读全文
posted @ 2018-04-17 22:46 wwfy 阅读(35189) 评论(0) 推荐(0) 编辑
点击右上角即可分享
微信分享提示