文档章节

Java中定时任务的实现:Timer与ScheduledExecutorService的不同

wf78728381
 wf78728381
发布于 2017/10/18 17:33
字数 3232
阅读 25
收藏 3

前言

在做后台任务的时候经常需要实现各种各种的定时的,周期性的任务。比如每隔一段时间更新一下缓存之类的。通常周期性的任务都可以使用如下方式实现:

  1. class MyTimerThread extends Thread {  
  2.     @Override   
  3.     public void run() {  
  4.         while(true) {  
  5.             try {  
  6.                 Thread.sleep(60*1000);  
  7.                   
  8.                 //每隔1分钟需要执行的任务  
  9.                 doTask();  
  10.                   
  11.             } catch (Exception e) {  
  12.                 e.printStackTrace();  
  13.             }  
  14.         }  
  15.     };  
  16. }  
class MyTimerThread extends Thread {
        @Override
        public void run() {
            while(true) {
                try {
                    Thread.sleep(60*1000);

                    //每隔1分钟需要执行的任务
                    doTask();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
    }
其实用这种方式我还没遇到过什么问题。网上有人说调用线程sleep()方法会导致线程休眠时还是会占用cpu资源不释放(而wait()不会),这种说法应该是不正确的。若有人知道其中存在的问题,敬请告知!由于这种实现一般都是一个线程对于一个定时任务,且没有实现在指定时间启动任务(也可以实现,加个时间判断就可以了)。


Timer简介

JDK提供的Timer是很常用的定时任务调度器。在说到timer的原理时,我们先看看Timer里面的一些常见方法:
  1. /** 
  2.  * 这个方法是调度一个task,经过delay(ms)后开始进行调度,仅仅调度一次 
  3.  */  
  4. public void schedule(TimerTask task, long delay)  
  5.   
  6. /** 
  7.  * 在指定的时间点time上调度一次 
  8.  */  
  9. public void schedule(TimerTask task, Date time)  
  10. 在指定的时间点time上调度一次。  
  11.   
  12. /** 
  13.  * 周期性调度任务,在delay(ms)后开始调度。 
  14.  * 并且任务开始时间的间隔为period(ms),即“固定间隔”执行 
  15.  */  
  16. public void schedule(TimerTask task, long delay, long period)  
  17.   
  18. /** 
  19.  * 和上一个方法类似,唯一的区别就是传入的第二个参数为第一次调度的时间 
  20.  */  
  21. public void schedule(TimerTask task, Date firstTime, long period)  
  22.   
  23.   
  24. public void scheduleAtFixedRate(TimerTask task, long delay, long period)  
  25.   
  26. public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)  
/**
 * 这个方法是调度一个task,经过delay(ms)后开始进行调度,仅仅调度一次
 */
public void schedule(TimerTask task, long delay)

/**
 * 在指定的时间点time上调度一次
 */
public void schedule(TimerTask task, Date time)
在指定的时间点time上调度一次。

/**
 * 周期性调度任务,在delay(ms)后开始调度。
 * 并且任务开始时间的间隔为period(ms),即“固定间隔”执行
 */
public void schedule(TimerTask task, long delay, long period)

/**
 * 和上一个方法类似,唯一的区别就是传入的第二个参数为第一次调度的时间
 */
public void schedule(TimerTask task, Date firstTime, long period)


public void scheduleAtFixedRate(TimerTask task, long delay, long period)

public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
不过比较不好理解的是Timer中,存在schedule和scheduleAtFixedRate两套不同调度算法的方法, 它们的共同点是若判断理论执行时间小于实际执行时间时,都会马上执行任务,区别在于计算下一次执行时间的方式不同:
schedule: 任务开始的时间 + period(时间片段),强调“固定间隔”地执行任务
scheduleAtFixedRate: 参数设定开始的时间 + period(时间片段),强调“固定频率”地执行任务
可以看出前者采用实际值,后者采用理论值。不过实际上若参数设定的开始时间比当前时间大的话,两者执行的效果是一样的。举个反例说明:
  1. public static void main(String[] args) {  
  2.       
  3.     TimerTask task = new TimerTask() {  
  4.         @Override   
  5.         public void run() {  
  6.             System.out.println(”do task…….”);  
  7.         }  
  8.     };  
  9.       
  10.     SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”);  
  11.     Timer timer = new Timer();  
  12.     try {  
  13.           
  14.         timer.schedule(task, sdf.parse(”2016-4-9 00:00:00”), 5000);  
  15.           
  16.         //timer.scheduleAtFixedRate(task, sdf.parse(“2016-4-9 00:00:00”),5000);  
  17.   
  18.     } catch (ParseException e) {  
  19.         e.printStackTrace();  
  20.     }  
  21. }  
public static void main(String[] args) {

        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("do task.......");
            }
        };

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Timer timer = new Timer();
        try {

            timer.schedule(task, sdf.parse("2016-4-9 00:00:00"), 5000);

            //timer.scheduleAtFixedRate(task, sdf.parse("2016-4-9 00:00:00"),5000);

        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
以上是参数设定时间比当前时间小的情况,我在2016-4-9 00:00:20时才启动上面的程序:
对于schedule,打印了1条”do task”。因为理论执行时间(00:00:00)小于实际执行时间(00:00:20)。然后等,因为下一次执行的时间为00:00:25。
对于scheduleAtFixedRate,打印了4条”do task”。因为它的理论执行时间分别是00:00:05、00:00:10、00:00:15、00:00:20、00:00:25……现在知道固定频率的意思了吧!说好了要执行多少次就是多少次。

Timer的缺陷

Timer被设计成支持多个定时任务,通过源码发现它有一个任务队列用来存放这些定时任务,并且启动了一个线程来处理,如下部分源码所示:
  1. public class Timer {  
  2.   
  3.     // 任务队列  
  4.     private final TaskQueue queue = new TaskQueue();  
  5.   
  6.     // 处理线程  
  7.     private final TimerThread thread = new TimerThread(queue);  
public class Timer {

    // 任务队列
    private final TaskQueue queue = new TaskQueue();

    // 处理线程
    private final TimerThread thread = new TimerThread(queue);
通过这种单线程的方式实现,在存在多个定时任务的时候便会存在问题: 若任务B执行时间过长,将导致任务A延迟了启动时间!
还存在另外一个问题,应该是属于设计的问题: 若任务线程在执行队列中某个任务时,该任务抛出异常,将导致线程因跳出循环体而终止,即Timer停止了工作!
同样是举个栗子:
  1. public static void main(String[] args) {  
  2.       
  3.     Timer timer = new Timer();  
  4.       
  5.     timer.schedule(new TimerTask() {  
  6.         @Override   
  7.         public void run() {  
  8.             SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);  
  9.             System.out.println(sdf.format(new Date()) + “ A: do task”);  
  10.         }  
  11.     }, 05*1000);  
  12.       
  13.     timer.schedule(new TimerTask() {  
  14.         @Override   
  15.         public void run() {  
  16.             SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);  
  17.             System.out.println(sdf.format(new Date()) + “ B: sleep”);  
  18.             try {  
  19.                 Thread.sleep(20*1000);  
  20.             } catch (InterruptedException e) {  
  21.                 e.printStackTrace();  
  22.             }  
  23.         }  
  24.     }, 10*10005000);  
  25.       
  26.     timer.schedule(new TimerTask() {  
  27.         @Override   
  28.         public void run() {  
  29.             SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);  
  30.             System.out.println(sdf.format(new Date()) + “ C: throw Exception”);  
  31.             throw new RuntimeException(“test”);  
  32.         }  
  33.     }, 30*10005000);  
  34. }  
public static void main(String[] args) {

        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                System.out.println(sdf.format(new Date()) + " A: do task");
            }
        }, 0, 5*1000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                System.out.println(sdf.format(new Date()) + " B: sleep");
                try {
                    Thread.sleep(20*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 10*1000, 5000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                System.out.println(sdf.format(new Date()) + " C: throw Exception");
                throw new RuntimeException("test");
            }
        }, 30*1000, 5000);
    }
通过以上程序发现:一开始,任务A能正常每隔5秒运行一次。在任务B启动后,由于任务B运行时间需要20秒,导致任务A要等到任务B执行完才能执行。更可怕的是,任务C启动后,抛了个异常,定时任务挂了!
不过这种单线程的实现也有优点:线程安全!

ScheduledThreadPoolExecutor简介

ScheduledThreadPoolExecutor可以说是Timer的多线程实现版本,连JDK官方都推荐使用ScheduledThreadPoolExecutor替代Timer。它是接口ScheduledExecutorService的子类,主要方法说明如下:
  1. /** 
  2.  * 调度一个task,经过delay(时间单位由参数unit决定)后开始进行调度,仅仅调度一次 
  3.  */  
  4. public ScheduledFuture<?> schedule(Runnable command,  
  5.                                        long delay, TimeUnit unit);  
  6.   
  7. /** 
  8.  * 同上,支持参数不一样 
  9.  */  
  10. public <V> ScheduledFuture<V> schedule(Callable<V> callable,  
  11.                                            long delay, TimeUnit unit);  
  12.   
  13. /** 
  14.  * 周期性调度任务,在delay后开始调度,适合执行时间比“间隔”短的任务 
  15.  * 并且任务开始时间的间隔为period,即“固定间隔”执行。 
  16.  * 如果任务执行的时间比period长的话,会导致该任务延迟执行,不会同时执行! 
  17.  * 如果任务执行过程抛出异常,后续不会再执行该任务! 
  18.  */  
  19. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,  
  20.                         long initialDelay ,long period ,TimeUnit unit);  
  21.   
  22. /** 
  23.  * Timer所没有的“特色”方法,称为“固定延迟(delay)”调度,适合执行时间比“间隔”长的任务 
  24.  * 在initialDelay后开始调度该任务 
  25.  * 随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟period 
  26.  * 即下一次任务开始的时间为:上一次任务结束时间(而不是开始时间) + delay时间 
  27.  * 如果任务执行过程抛出异常,后续不会再执行该任务! 
  28.  */  
  29. public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,  
  30.                         long initialDelay ,long delay ,TimeUnit unit);  
/**
 * 调度一个task,经过delay(时间单位由参数unit决定)后开始进行调度,仅仅调度一次
 */
public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

/**
 * 同上,支持参数不一样
 */
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);

/**
 * 周期性调度任务,在delay后开始调度,适合执行时间比“间隔”短的任务
 * 并且任务开始时间的间隔为period,即“固定间隔”执行。
 * 如果任务执行的时间比period长的话,会导致该任务延迟执行,不会同时执行!
 * 如果任务执行过程抛出异常,后续不会再执行该任务!
 */
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                        long initialDelay ,long period ,TimeUnit unit);

/**
 * Timer所没有的“特色”方法,称为“固定延迟(delay)”调度,适合执行时间比“间隔”长的任务
 * 在initialDelay后开始调度该任务
 * 随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟period
 * 即下一次任务开始的时间为:上一次任务结束时间(而不是开始时间) + delay时间
 * 如果任务执行过程抛出异常,后续不会再执行该任务!
 */
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                        long initialDelay ,long delay ,TimeUnit unit);

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以本质上说ScheduledThreadPoolExecutor还是一个线程池(可参考 《Java线程池ThreadPoolExecutor简介》)。它也有coorPoolSize和workQueue,接受Runnable的子类作为任务。
特殊的地方在于它实现了自己的工作队列DelayedWorkQueue,该任务队列的作用是按照一定顺序对队列中的任务进行排序。比如,按照距离下次执行时间的长短的升序方式排列,让需要尽快执行的任务排在队首,“不那么着急”的任务排在队列后方,从而方便线程获取到“应该”被执行的任务。除此之外,ScheduledThreadPoolExecutor还在任务执行结束后,计算出下次执行的时间,重新放到工作队列中,等待下次调用。

上面通过一个程序说明了Timer存在的问题!这里我将Timer换成了用ScheduledThreadPoolExecutor来实现,注意TimerTask也是Runnable的子类。
  1. public static void main(String[] args) {  
  2.     int corePoolSize = 3;  
  3.     ScheduledExecutorService pool = Executors.newScheduledThreadPool(corePoolSize);    
  4.          
  5.        pool.scheduleAtFixedRate(new TimerTask() {  
  6.         @Override  
  7.         public void run() {  
  8.             SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);  
  9.             System.out.println(sdf.format(new Date()) + “ A: do task”);  
  10.         }  
  11.     }, 0 ,5, TimeUnit.SECONDS);    
  12.       
  13.        pool.scheduleAtFixedRate(new TimerTask() {  
  14.         @Override  
  15.         public void run() {  
  16.             SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);  
  17.             System.out.println(sdf.format(new Date()) + “ B: sleep”);  
  18.             try {  
  19.                 Thread.sleep(20*1000);  
  20.             } catch (InterruptedException e) {  
  21.                 e.printStackTrace();  
  22.             }  
  23.         }  
  24.     }, 105, TimeUnit.SECONDS);  
  25.       
  26.        pool.scheduleAtFixedRate(new TimerTask() {  
  27.         @Override  
  28.         public void run() {  
  29.             SimpleDateFormat sdf = new SimpleDateFormat(“HH:mm:ss”);  
  30.             System.out.println(sdf.format(new Date()) + “ C: throw Exception”);  
  31.             throw new RuntimeException(“test”);  
  32.         }  
  33.     }, 305, TimeUnit.SECONDS);  
  34. }  
public static void main(String[] args) {
        int corePoolSize = 3;
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(corePoolSize);  

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                System.out.println(sdf.format(new Date()) + " A: do task");
            }
        }, 0 ,5, TimeUnit.SECONDS);  

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                System.out.println(sdf.format(new Date()) + " B: sleep");
                try {
                    Thread.sleep(20*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 10, 5, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                System.out.println(sdf.format(new Date()) + " C: throw Exception");
                throw new RuntimeException("test");
            }
        }, 30, 5, TimeUnit.SECONDS);
    }
由于有3个任务需要调度,因此我将corePoolSize设置为3。通过控制台打印可以看到这次任务A一直都在正常运行(任务时间间隔为5秒),并不受任务B的影响。任务C抛出异常后,虽然本身停止了调度,但没有影响到其他任务的调度。可以说ScheduledThreadPoolExecutor解决Timer存在的问题!
那要是将corePoolSize设置为1,变成单线程跑呢?结果当然是和Timer一样,任务B会导致任务A延迟执行,不过比较好的是任务C抛异常不会影响到其他任务的调度。

可以说ScheduledThreadPoolExecutor适用于大部分场景,甚至就算timer提供的Date参数类型的开始时间也可以通过自己转的方式来实现。任务调度框架Quatz也是在ScheduledThreadPoolExecutor基础上实现的。

一般我们都使用单线程版的ScheduledThreadPoolExecutor居多,推荐通过以下方式来构建(构建后其线程数就不可更改):
  1. ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();  
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();


总结

很多时候真的不可能记得住这些类库的特性,一不小心就会踩坑!比如我上面反复强调的要是任务执行过程抛出异常了会怎么怎么样,其实人家的API注释是有说明的。另外是不确定的还是用通过写demo来实践一下,看看是不是真的这样!还有就是除了看资料,写demo,还可以了解底层实现,这样了解得更透彻。比如在若只有一个任务需要调度的情况下,其实就算用Timer也是可以的。
如上文有不正确的地方,感谢指点出来!



参考


本文转载自:http://blog.csdn.net/wf787283810/article/details/78028430

wf78728381
粉丝 0
博文 26
码字总数 0
作品 0
临沂
程序员
私信 提问
SpringBoot | 第二十二章:定时任务的使用

前言 上两章节,我们简单的讲解了关于异步调用和异步请求相关知识点。这一章节,我们来讲讲开发过程也是经常会碰见的定时任务。比如每天定时清理无效数据、定时发送短信、定时发送邮件、支付...

oKong
2018/08/19
1K
3
Java实现定时任务的三种方法

在本文里,我会给大家介绍3种不同的实现方法: 普通thread实现 TimerTask实现 ScheduledExecutorService实现 普通thread   这是最常见的,创建一个thread,然后让它在while循环里一直运行着...

markGao
2014/04/09
58
0
SpringBoot基础教程3-1-1 简单定时任务方法介绍

1 概述 定时任务,特定的时间执行任务,在项目中普遍使用。本文介绍简单的无状态无持久化的定时任务实现方式;,,。 2 实现方式 自带的;通过调度的方式 让程序按照某一个频度执行,但不能在...

Mkeeper
2018/09/29
61
0
Java实现定时任务的三种方法

普通thread 这是最常见的,创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果。这样可以快速简单的实现,代码如下: public class Task1 {public stati...

白志华
2015/07/22
320
0
Java 定时任务系列(1)- Java原生支持

1、普通thread实现 这是最常见的,创建一个thread,然后让它在while循环里一直运行着,通过sleep方法来达到定时任务的效果。这样可以快速简单的实现,代码如下: 2、用Timer和TimerTask 介绍...

Jacendfeng
09/29
0
0

没有更多内容

加载失败,请刷新页面

加载更多

500行代码,教你用python写个微信飞机大战

这几天在重温微信小游戏的飞机大战,玩着玩着就在思考人生了,这飞机大战怎么就可以做的那么好,操作简单,简单上手。 帮助蹲厕族、YP族、饭圈女孩在无聊之余可以有一样东西让他们振作起来!...

上海小胖
18分钟前
1
0
关于AsyncTask的onPostExcute方法是否会在Activity重建过程中调用的问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/XG1057415595/article/details/86774575 假设下面一种情况...

shzwork
今天
7
0
object 类中有哪些方法?

getClass(): 获取运行时类的对象 equals():判断其他对象是否与此对象相等 hashcode():返回该对象的哈希码值 toString():返回该对象的字符串表示 clone(): 创建并返此对象的一个副本 wait...

happywe
今天
6
0
Docker容器实战(七) - 容器中进程视野下的文件系统

前两文中,讲了Linux容器最基础的两种技术 Namespace 作用是“隔离”,它让应用进程只能看到该Namespace内的“世界” Cgroups 作用是“限制”,它给这个“世界”围上了一圈看不见的墙 这么一...

JavaEdge
今天
8
0
文件访问和共享的方法介绍

在上一篇文章中,你了解到文件有三个不同的权限集。拥有该文件的用户有一个集合,拥有该文件的组的成员有一个集合,然后最终一个集合适用于其他所有人。在长列表(ls -l)中这些权限使用符号...

老孟的Linux私房菜
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部