zoukankan      html  css  js  c++  java
  • 定时且周期性的任务研究I--Timer

    很多时候我们希望任务可以定时的周期性的执行,在最初的JAVA工具类库中,通过Timer可以实现定时的周期性的需求,但是有一定的缺陷,例如:Timer是基于绝对时间的而非支持相对时间,因此Timer对系统时钟比较敏感。虽然有一定的问题,但是我们还是从这个最简单的实现开始研究。

    首先,我们准备一些讨论问题的类:TimerTask1和TimerLongTask,如下

    Java代码  收藏代码
    1. public class TimerTask1 extends TimerTask {  
    2.   
    3.     @Override  
    4.     public void run() {  
    5.         String base = "abcdefghijklmnopqrstuvwxyz0123456789";  
    6.         Random random = new Random();  
    7.         StringBuffer sb = new StringBuffer();  
    8.         for (int i = 0; i < 10; i++) {  
    9.             int number = random.nextInt(base.length());  
    10.             sb.append(base.charAt(number));  
    11.         }  
    12.         System.out.println(new Date()+","+sb.toString());  
    13.     }  
    14.   
    15. }  

     这个类负责生成一个含有10个字符的字符串,这里我们将输出时间打印出来,近似认为是任务执行的时间。

    Java代码  收藏代码
    1. public class TimerLongTask extends TimerTask {  
    2.   
    3.     @Override  
    4.     public void run() {  
    5.         System.out.println("TimerLongTask: 开始沉睡");  
    6.         try {  
    7.             TimeUnit.SECONDS.sleep(10);  
    8.         } catch (InterruptedException e) {  
    9.             e.printStackTrace();  
    10.         }  
    11.         System.out.println("TimerLongTask: 已经醒来");  
    12.     }  
    13.   
    14. }  

     这个类启动了一个长任务,即让任务沉睡10秒。

    下面我们来看一个定时任务执行的例子:

    Java代码  收藏代码
    1. public static void main(String[] args) throws InterruptedException {  
    2.     Timer timer = new Timer();  
    3.     timer.schedule(new TimerTask1(), 1);  
    4.     timer.schedule(new TimerLongTask(), 1);  
    5.     timer.schedule(new TimerTask1(), 2);  
    6.     TimeUnit.SECONDS.sleep(20);  
    7.     timer.cancel();  
    8. }  

     在这个例子中,我们先提交了一个TimerTask1任务,且让它延迟1毫秒执行,紧接着我们又提交了一个TimerLongTask长任务,且让它也延迟1毫秒执行,最后我们在提交一个TimerTask1任务,延迟2毫秒执行。然后让主线程沉睡20秒后关闭timer。我们看一下执行结果:

    Java代码  收藏代码
    1. Thu Apr 21 11:04:31 CST 2011,utg3hn7u4r  
    2. TimerLongTask: 开始沉睡  
    3. TimerLongTask: 已经醒来  
    4. Thu Apr 21 11:04:41 CST 2011,4aac22sud1  

     这里我们看到第一次输出10个字符的时间和第二次输出10个字符的时间上相差了10秒,这10秒恰恰是长任务沉睡的时间,通过这个输出我们可以分析出:Timer用来执行任务的线程其实只有一个,且逐一被执行。接下来我们查看一下源码验证一下,如下:

    Java代码  收藏代码
    1. private TaskQueue queue = new TaskQueue();  
    2. private TimerThread thread = new TimerThread(queue);  

     这两行代码来自Timer源码,我们可以看到在第一次创建了Timer时就已经创建了一个thread和一个queue,因此只有一个线程来执行我们的任务。

    那么Timer是如何来执行任务的?

    首先我们调用timer.schedule方法,将任务提交到timer中,Timer中有很多重载的schedule方法,但它们都会调用同一个方法即sched方法。这个方法会将我们提交的任务添加到TaskQueue的队列中(即queue),在每次添加时都会根据nextExecutionTime大小来调整队列中任务的顺序,让nextExecutionTime最小的排在队列的最前端,nextExecutionTime最大的排在队列的最后端。在创建Timer时,我们同时也创建了一个TimerThread即thread,并且启动了这个线程,

    Java代码  收藏代码
    1. public Timer(String name) {  
    2.         thread.setName(name);  
    3.         thread.start();  
    4. }  

     TimerThread中的mainLoop方法是核心,它会完成所有的任务执行,在一开始我们的队列为空,这时mainLoop方法将会使线程进入等待状态,当我们使用schedule提交任务时会notify这个TimerThread线程,若任务的执行未到则在wait相对的时间差。

    我们调整一下上面的代码,

    Java代码  收藏代码
    1. Timer timer = new Timer();  
    2. timer.schedule(new TimerTask1(), 1);  
    3. timer.schedule(new TimerTask1(), 5000);  
    4. timer.schedule(new TimerLongTask(), 3000);  
    5. TimeUnit.SECONDS.sleep(20);  
    6. timer.cancel();  

     这样先提交两个输出字符的任务最后提交长任务,在这里,我们让第二个输出字符的任务延迟5秒执行,长任务延迟3秒执行,这样得到的结果如下:

    Java代码  收藏代码
    1. Thu Apr 21 13:07:44 CST 2011,2sstwluvgc  
    2. TimerLongTask: 开始沉睡  
    3. TimerLongTask: 已经醒来  
    4. Thu Apr 21 13:07:57 CST 2011,sh4fnkqqc8  

     虽然我们改变了提交顺序,但是还是按照延迟时间递增排序执行的,两个输出字符串的时间之间相差13秒,这也是长任务等待执行时间+长任务睡眠时间之和。

    重复执行scheduleAtFixedRate方法提交任务,主要是调用rescheduleMin方法对已经调用的任务进行重新设置调度延迟,并调用fixDown方法对队列里的任务根据延迟时间重新排序。

    Java代码  收藏代码
    1. Timer timer = new Timer();  
    2. timer.scheduleAtFixedRate(new TimerTask1(), 3000, 5000);  

     3000,代表第一次执行时等待的时间,5000代表每次执行任务之间的时间间隔,运行结果:

    Java代码  收藏代码
    1. Thu Apr 21 13:14:55 CST 2011,izf536esrg  
    2. Thu Apr 21 13:15:00 CST 2011,2khzm7e09v  
    3. Thu Apr 21 13:15:05 CST 2011,jc3dvt2m8q  

     基本是每5秒运行一次。

    由于Timer只使用一个线程运行所有的任务,那么当一个任务抛出运行时异常后会有什么样的情形呢?其他的任务是否可以继续?我们已经有了前面的知识可以先猜想一个结果:因为Timer只使用一个线程运行所有的任务,所以当一个线程抛出运行时异常时,这个线程就基本挂了,不会在执行后续的任何代码,因此我们可以断言,当一个任务抛出运行时异常时,后续任务都不可以执行。为了证明这个猜想,我们需要一个可以抛出异常的任务,如下:

    Java代码  收藏代码
    1. public class TimerExceptionTask extends TimerTask {  
    2.   
    3.     @Override  
    4.     public void run() {  
    5.         System.out.println("TimerExceptionTask: "+new Date());  
    6.         throw new RuntimeException();  
    7.     }  
    8.   
    9. }  

     这个任务抛出一个运行时异常。接着我们需要定义一下我们任务执行的顺序:先执行一个正常的任务,然后在执行一个抛出异常的任务,最后在执行一个正常的任务,如下:

    Java代码  收藏代码
    1. Timer timer = new Timer();  
    2. timer.schedule(new TimerTask1(), 1000);  
    3. timer.schedule(new TimerExceptionTask(), 3000);  
    4. timer.schedule(new TimerTask1(), 5000);  
    5. TimeUnit.SECONDS.sleep(6);  
    6. timer.cancel();  

     延迟1秒执行正常输出字符串的任务,延迟3秒执行抛出异常的任务,延迟5秒执行正常输出字符串的任务,看一下结果:

    Java代码  收藏代码
    1. Thu Apr 21 13:40:23 CST 2011,lk7fjneyyu  
    2. TimerExceptionTask: Thu Apr 21 13:40:25 CST 2011  
    3. Exception in thread "Timer-0" java.lang.RuntimeException  
    4.     at org.victorzhzh.concurrency.TimerExceptionTask.run(TimerExceptionTask.java:11)  
    5.     at java.util.TimerThread.mainLoop(Timer.java:512)  
    6.     at java.util.TimerThread.run(Timer.java:462)  

     并没有输出两个字符串,只执行了第一次的输出字符串任务,说明当抛出运行时异常时,其后续任务不可能被执行。

    鉴于Timer的缺陷,所以对它的使用还是要谨慎的,还好并发包中为我们提供了相应的替代品:ScheduledThreadPoolExecutor。

  • 相关阅读:
    POJ 3660 Cow Contest (floyd求联通关系)
    POJ 3660 Cow Contest (最短路dijkstra)
    POJ 1860 Currency Exchange (bellman-ford判负环)
    POJ 3268 Silver Cow Party (最短路dijkstra)
    POJ 1679 The Unique MST (最小生成树)
    POJ 3026 Borg Maze (最小生成树)
    HDU 4891 The Great Pan (模拟)
    HDU 4950 Monster (水题)
    URAL 2040 Palindromes and Super Abilities 2 (回文自动机)
    URAL 2037 Richness of binary words (回文子串,找规律)
  • 原文地址:https://www.cnblogs.com/senior-engineer/p/5000191.html
Copyright © 2011-2022 走看看