Java 定时任务实现原理详解+SSM框架应用定时器

Java 定时任务实现原理详解+SSM框架应用定时器

在jdk自带的库中,有两种技术可以实现定时任务。一种是使用Timer,另外一个则是ScheduledThreadPoolExecutor。下面为大家分析一下这两个技术的底层实现原理以及各自的优缺点。

一、Timer

1. Timer的使用

class MyTask extends TimerTask{
    @Override
    public void run() {
        System.out.println("hello world");
    }
}
public class TimerDemo {
    public static void main(String[] args) {
        //创建定时器对象
        Timer t=new Timer();
        //在3秒后执行MyTask类中的run方法,后面每10秒跑一次
        t.schedule(new MyTask(), 3000,10000);

    }
}

通过往Timer提交一个TimerTask的任务,同时指定多久后开始执行以及执行周期,我们可以开启一个定时任务。

2. 源码解析
首先我们先来看一下Timer这个类

//存放定时任务的队列
//这个TaskQueue 也是Timer内部自定义的一个队列,这个队列通过最小堆来维护队列
//下一次执行时间距离现在最小的会被放在堆顶,到时执行线程直接获取堆顶任务并判断是否执行即可
private final TaskQueue queue = new TaskQueue();
//负责执行定时任务的线程
private final TimerThread thread = new TimerThread(queue);
public Timer() {
        this("Timer-" + serialNumber());
}
public Timer(String name) {
        //设置线程的名字,并且启动这个线程
        thread.setName(name);
        thread.start();
}

再来看一下TimerThread 这个类,这个类也是定义在Timer.class中的一个类,它继承了Thread类,所以可以直接拿来当线程使用。
我们直接来看他的构造方法以及run方法

//在Timer中初始化的时候会将Timer的Queue赋值进来
TimerThread(TaskQueue queue) {
        this.queue = queue;
}
public void run() {
        try {
            //进入自旋,开始不断的从任务队列中获取定时任务来执行
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
}
private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                //加同步
                synchronized(queue) {
                    //如果任务队列为空,并且newTasksMayBeScheduled为true,就休眠等待,直到有任务进来就会唤醒这个线程
                    //如果有人调用timer的cancel方法,newTasksMayBeScheduled会变成false
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; 

                    // 获取当前时间和下次任务执行时间
                    long currentTime, executionTime;
                    //获取队列中最早要执行的任务
                    task = queue.getMin();
                    synchronized(task.lock) {
                        //如果这个任务已经被结束了,就从队列中移除
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        //获取当前时间和下次任务执行时间
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        //判断任务执行时间是否小于当前时间,表示小于,就说明可以执行了
                        if (taskFired = (executionTime<=currentTime)) {
                            //如果任务的执行周期是0,说明只要执行一次就好了,就从队列中移除它,这样下一次就不会获取到该任务了
                            if (task.period == 0) {
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { 
                                //重新设置该任务下一次的执行时间
                                //如果之前设置的period小于0,就用当前时间-period,等于就是当前时间加上周期值
                                //这里的下次执行时间就是当前的执行时间加上周期值
                                //这里涉及到是否以固定频率调用任务的问题,下面再详细讲解
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    //如果任务的执行时间还没到,就计算出还有多久才到达执行时间,然后线程进入休眠
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                //如果任务的执行时间到了,就执行这个任务
                if (taskFired)
                    task.run();
            } catch(InterruptedException e) {
            }
        }
}

通过上面的代码,我们大概了解了Timer是怎么工作的了。下面来看一下schedule()方法的相关代码

//在Timer中初始化的时候会将Timer的Queue赋值进来
TimerThread(TaskQueue queue) {
        this.queue = queue;
}
public void run() {
        try {
            //进入自旋,开始不断的从任务队列中获取定时任务来执行
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
}
private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                //加同步
                synchronized(queue) {
                    //如果任务队列为空,并且newTasksMayBeScheduled为true,就休眠等待,直到有任务进来就会唤醒这个线程
                    //如果有人调用timer的cancel方法,newTasksMayBeScheduled会变成false
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; 

                    // 获取当前时间和下次任务执行时间
                    long currentTime, executionTime;
                    //获取队列中最早要执行的任务
                    task = queue.getMin();
                    synchronized(task.lock) {
                        //如果这个任务已经被结束了,就从队列中移除
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        //获取当前时间和下次任务执行时间
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        //判断任务执行时间是否小于当前时间,表示小于,就说明可以执行了
                        if (taskFired = (executionTime<=currentTime)) {
                            //如果任务的执行周期是0,说明只要执行一次就好了,就从队列中移除它,这样下一次就不会获取到该任务了
                            if (task.period == 0) {
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { 
                                //重新设置该任务下一次的执行时间
                                //如果之前设置的period小于0,就用当前时间-period,等于就是当前时间加上周期值
                                //这里的下次执行时间就是当前的执行时间加上周期值
                                //这里涉及到是否以固定频率调用任务的问题,下面再详细讲解
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    //如果任务的执行时间还没到,就计算出还有多久才到达执行时间,然后线程进入休眠
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                //如果任务的执行时间到了,就执行这个任务
                if (taskFired)
                    task.run();
            } catch(InterruptedException e) {
            }
        }
}

3. 总结
Timer的原理比较简单,当我们初始化Timer的时候,timer内部会启动一个线程,并且初始化一个优先级队列,该优先级队列使用了最小堆的技术来将最早执行时间的任务放在堆顶。
当我们调用schedule方法的时候,其实就是生成一个任务然后插入到该优先级队列中。最后,timer内部的线程会从优先级队列的堆顶获取任务,获取到任务后,先判断执行时间是否到了,如果到了先设置下一次的执行时间并调整堆,然后执行任务。如果没到执行时间那线程就休眠一段时间。
关于计算下次任务执行时间的策略:
这里设置下一次执行时间的算法会根据传入peroid的值来判断使用哪种策略:

  • 如果peroid是负数,那下一次的执行时间就是当前时间+peroid的值
  • 如果peroid是正数,那下一次执行时间就是该任务这次的执行时间+peroid的值。
    这两个策略的不同点在于,如果计算下次执行时间是以当前时间为基数,那它就不是以固定频率来执行任务的。因为Timer是单线程执行任务的,如果A任务执行周期是10秒,但是有个B任务执行了20几秒,那么下一次A任务的执行时间就要等B执行完后轮到自己时,再过10秒才会执行下一次。
    如果策略是这次任务的执行时间+peroid的值就是按固定频率不断执行任务了。读者可以自行模拟一下

二、ScheduledThreadPoolExecutor

1. 使用

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8);
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello world");
            }
}, 1, 3, TimeUnit.SECONDS);

2. 实现原理+源码解析
由于ScheduledThreadPoolExecutor是基于线程池实现的。所以了解它的原理之前读者有必要先了解一下Java线程池的实现。

我们直接来看一下的源码

//在Timer中初始化的时候会将Timer的Queue赋值进来
TimerThread(TaskQueue queue) {
        this.queue = queue;
}
public void run() {
        try {
            //进入自旋,开始不断的从任务队列中获取定时任务来执行
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
}
private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                //加同步
                synchronized(queue) {
                    //如果任务队列为空,并且newTasksMayBeScheduled为true,就休眠等待,直到有任务进来就会唤醒这个线程
                    //如果有人调用timer的cancel方法,newTasksMayBeScheduled会变成false
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; 

                    // 获取当前时间和下次任务执行时间
                    long currentTime, executionTime;
                    //获取队列中最早要执行的任务
                    task = queue.getMin();
                    synchronized(task.lock) {
                        //如果这个任务已经被结束了,就从队列中移除
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        //获取当前时间和下次任务执行时间
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        //判断任务执行时间是否小于当前时间,表示小于,就说明可以执行了
                        if (taskFired = (executionTime<=currentTime)) {
                            //如果任务的执行周期是0,说明只要执行一次就好了,就从队列中移除它,这样下一次就不会获取到该任务了
                            if (task.period == 0) {
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { 
                                //重新设置该任务下一次的执行时间
                                //如果之前设置的period小于0,就用当前时间-period,等于就是当前时间加上周期值
                                //这里的下次执行时间就是当前的执行时间加上周期值
                                //这里涉及到是否以固定频率调用任务的问题,下面再详细讲解
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    //如果任务的执行时间还没到,就计算出还有多久才到达执行时间,然后线程进入休眠
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                //如果任务的执行时间到了,就执行这个任务
                if (taskFired)
                    task.run();
            } catch(InterruptedException e) {
            }
        }
}

到这里,我们可以看到我们提交的任务被封装成一个ScheduledFutureTask然后提交给任务队列,同时如果发现worker的数量少于设置的corePoolSize,我们还会启动一个worker线程。
但是,我们怎么保证worker不会马上就从任务队列中获取任务然后直接执行呢(这样我们设定的延迟执行就没有效果了)?
另外,怎么保证任务执行完下一次在一定周期后还会再执行呢,也就是怎么保证任务的延迟执行和周期执行?
我们先来看一下任务的延迟执行的解决方案。其实就是修改任务队列的实现,通过将任务队列变成延迟队列,worker不会马上获取到任务队列中的任务了。只有任务的时间到了,worker线程才能从延迟队列中获取到任务并执行。
在ScheduledThreadPoolExecutor中,定义了DelayedWorkQueue类来实现延迟队列。DelayedWorkQueue内部使用了最小堆的数据结构,当任务插入到队列中时,会根据执行的时间自动调整在堆中的位置,最后执行时间最近的那个会放在堆顶。
当worker要去队列获取任务时,如果堆顶的执行时间还没到,那么worker就会阻塞一定时间后才能获取到那个任务,这样就实现了任务的延迟执行。
由于篇幅问题,DelayedWorkQueue的源码就不作解析了,有兴趣的朋友可以去ScheduledThreadPoolExecutor类中查阅。
解决了任务的延迟执行问题,接下来就是任务的周期执行的解决方案了。周期执行和前面封装的ScheduledFutureTask有关。我们直接来看一下ScheduledFutureTask的run方法就知道了

//在Timer中初始化的时候会将Timer的Queue赋值进来
TimerThread(TaskQueue queue) {
        this.queue = queue;
}
public void run() {
        try {
            //进入自旋,开始不断的从任务队列中获取定时任务来执行
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
}
private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                //加同步
                synchronized(queue) {
                    //如果任务队列为空,并且newTasksMayBeScheduled为true,就休眠等待,直到有任务进来就会唤醒这个线程
                    //如果有人调用timer的cancel方法,newTasksMayBeScheduled会变成false
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; 

                    // 获取当前时间和下次任务执行时间
                    long currentTime, executionTime;
                    //获取队列中最早要执行的任务
                    task = queue.getMin();
                    synchronized(task.lock) {
                        //如果这个任务已经被结束了,就从队列中移除
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        //获取当前时间和下次任务执行时间
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        //判断任务执行时间是否小于当前时间,表示小于,就说明可以执行了
                        if (taskFired = (executionTime<=currentTime)) {
                            //如果任务的执行周期是0,说明只要执行一次就好了,就从队列中移除它,这样下一次就不会获取到该任务了
                            if (task.period == 0) {
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { 
                                //重新设置该任务下一次的执行时间
                                //如果之前设置的period小于0,就用当前时间-period,等于就是当前时间加上周期值
                                //这里的下次执行时间就是当前的执行时间加上周期值
                                //这里涉及到是否以固定频率调用任务的问题,下面再详细讲解
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    //如果任务的执行时间还没到,就计算出还有多久才到达执行时间,然后线程进入休眠
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                //如果任务的执行时间到了,就执行这个任务
                if (taskFired)
                    task.run();
            } catch(InterruptedException e) {
            }
        }
}

从源码可以看出,当任务执行完后,如果该任务时周期性任务,那么会重新计算下一次执行时间,然后重新放到任务队列中等待下一次执行。

3. 总结
ScheduledThreadPoolExecutor的实现是基于java线程池。通过对任务进行一层封装来实现任务的周期执行,以及将任务队列改成延迟队列来实现任务的延迟执行。

我们将任务放入任务队列的同时,会尝试开启一个worker来执行这个任务(如果当前worker的数量小于corePoolSize)。由于这个任务队列时一个延迟队列,只有任务执行时间达到才能获取到任务,因此worker只能阻塞等到有队列中有任务到达才能获取到任务执行。

当任务执行完后,会检查自己是否是一个周期性执行的任务。如果是的话,就会重新计算下一次执行的时间,然后重新将自己放入任务队列中。

关于下一次任务的执行时间的计算规则,和Timer差不多,这里就不多做介绍。

三、Timer和ScheduledThreadPoolExecutor的区别

由于Timer是单线程的,如果一次执行多个定时任务,会导致某些任务被其他任务所阻塞。比如A任务每秒执行一次,B任务10秒执行一次,但是一次执行5秒,就会导致A任务在长达5秒都不会得到执行机会。而ScheduledThreadPoolExecutor是基于线程池的,可以动态的调整线程的数量,所以不会有这个问题

如果执行多个任务,在Timer中一个任务的崩溃会导致所有任务崩溃,从而所有任务都停止执行。而ScheduledThreadPoolExecutor则不会。

Timer的执行周期时间依赖于系统时间,timer中,获取到堆顶任务执行时间后,如果执行时间还没到,会计算出需要休眠的时间=(执行时间-系统时间),如果系统时间被调整,就会导致休眠时间无限拉长,后面就算改回来了任务也因为在休眠中而得不到执行的机会。ScheduledThreadPoolExecutor由于用是了nanoTime来计算执行周期的,所以和系统时间是无关的,无论系统时间怎么调整都不会影响到任务调度。

注意的是,nanoTime和系统时间是完全无关的(之前一直以为只是时间戳的纳秒级粒度),关于nanoTime的介绍如下:

返回最准确的可用系统计时器的当前值,以毫微秒为单位。
此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数(或许从以后算起,所以该值可能为负)。此方法提供毫微秒的精度,但不是必要的毫微秒的准确度。它对于值的更改频率没有作出保证。在取值范围大于约 292 年(263 毫微秒)的连续调用的不同点在于:由于数字溢出,将无法准确计算已过的时间。

总体来说,Timer除了在版本兼容性上面略胜一筹以外(Timer是jdk1.3就支持的,而ScheduledThreadPoolExecutor在jdk1.5才出现),其余全部被ScheduledThreadPoolExecutor碾压。所以日常技术选型中,也推荐使用ScheduledThreadPoolExecutor来实现定时任务。
原文链接:https://blog.csdn.net/u013332124/article/details/79603943#commentBox

四、SSM框架SpringMVC@Scheduled注解简单实现

第一步:

在Springmvc的xml中加入如下:

xmlns:task="http://www.springframework.org/schema/task"

http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.1.xsd">

第二步:
在中间加上:

    <!-- 任务调度器 --> 

<task:scheduler id="scheduler" pool-size="10" />

<!--开启注解调度支持 @scheduled -->  

<task:annotation-driven executor="scheduler" proxy-target-class="true"/>

例如:
在这里插入图片描述
第三步:

编写测试类:需要在类加@component注解,但是本人在@controller测试可行,@Component反而无法导入@Scheduled包不知为何,但是强制导入也可行,如:

在这里插入图片描述

/**
* 每天晚上23点执行查询第三方数据任务

* @throws Exception 
*/

@Scheduled(cron = "0 0 23 * * ?")

public void Scheduled() throws Exception{

System.out.println("每天的23:00时间到了,开始调用第三方接口查询流水任务咯");

 }

说明:(cron = “0 0 23 * * ?”)可自行百度,如:(cron = “0/5 * * * * ?”)为每5秒执行一次
上图:
在这里插入图片描述

五、corn表达式

一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素。
按顺序依次为
秒(0~59)
分钟(0~59)
小时(0~23)
天(月)(0~31,但是你需要考虑你月的天数)
月(0~11)
天(星期)(1~7 1=SUN 或 SUN,MON,TUE,WED,THU,FRI,SAT)
7.年份(1970-2099)

其中每个元素可以是一个值(如6),一个连续区间(9-12),一个间隔时间(8-18/4)(/表示每隔4小时),一个列表(1,3,5),通配符。由于"月份中的日期"和"星期中的日期"这两个元素互斥的,必须要对其中一个设置?.

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 12 ? * WED 表示每个星期三中午12点
“0 0 12 * * ?” 每天中午12点触发
“0 15 10 ? * *” 每天上午10:15触发
“0 15 10 * * ?” 每天上午10:15触发
“0 15 10 * * ? *” 每天上午10:15触发
“0 15 10 * * ? 2005” 2005年的每天上午10:15触发
“0 * 14 * * ?” 在每天下午2点到下午2:59期间的每1分钟触发
“0 0/5 14 * * ?” 在每天下午2点到下午2:55期间的每5分钟触发
“0 0/5 14,18 * * ?” 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
“0 0-5 14 * * ?” 在每天下午2点到下午2:05期间的每1分钟触发
“0 10,44 14 ? 3 WED” 每年三月的星期三的下午2:10和2:44触发
“0 15 10 ? * MON-FRI” 周一至周五的上午10:15触发
“0 15 10 15 * ?” 每月15日上午10:15触发
“0 15 10 L * ?” 每月最后一日的上午10:15触发
“0 15 10 ? * 6L” 每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6L 2002-2005” 2002年至2005年的每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6#3” 每月的第三个星期五上午10:15触发

有些子表达式能包含一些范围或列表

例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”

“*”字符代表所有可能的值

因此,“”在子表达式(月)里表示每个月的含义,“”在子表达式(天(星期))表示星期的每一天
“/”字符用来指定数值的增量

例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟

在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样

“?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值

当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”

“L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写

但是它在两个子表达式里的含义是不同的。

在天(月)子表达式中,“L”表示一个月的最后一天

在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT

如果在“L”前有具体的内容,它就具有其他的含义了

例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五

注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值