定时且周期性的任务研究I--Timer ---- 任务研究II--ScheduledThreadPoolExecutor

(转)定时且周期性的任务研究I--Timer ---- 任务研究II--ScheduledThreadPoolExecutor

jdk并发部分,如果英文理解有点小难,可以参考http://www.yiibai.com/java6/java/util/concurrent/package-summary.html
本篇转自:http://victorzhzh.iteye.com/blog/1011635

很多时候我们希望任务可以定时的周期性的执行,在最初的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(), 30005000);  

 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是如何优化的。

为了研究方便我们需要两个类:

Java代码    收藏代码
  1. public class Task1 implements Callable<String> {  
  2.   
  3.     @Override  
  4.     public String call() throws Exception {  
  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("Task1 running: " + new Date());  
  13.         return sb.toString();  
  14.     }  
  15.   
  16. }  

 生成含有10个字符的字符串,使用Callable接口目的是我们不再任务中直接输出结果,而主动取获取任务的结果

Java代码    收藏代码
  1. public class LongTask implements Callable<String> {  
  2.   
  3.     @Override  
  4.     public String call() throws Exception {  
  5.         System.out.println("LongTask running: "+new Date());  
  6.         TimeUnit.SECONDS.sleep(10);  
  7.         return "success";  
  8.     }  
  9.   
  10. }  

长任务类,这里我们让任务沉睡10秒然后返回一个“success”

下面我们来分析一下ScheduledThreadPoolExecutor:

1、Timer中单线程问题是否在ScheduledThreadPoolExecutor中存在?

我们先来看一下面的程序:

Java代码    收藏代码
  1. public class ScheduledThreadPoolExec {  
  2.     public static void main(String[] args) throws InterruptedException,  
  3.             ExecutionException {  
  4.         <strong><span style="color: #ff0000;">ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(  
  5.                 2);</span>  
  6. </strong>  
  7.   
  8.   
  9.         ScheduledFuture future1 = executor.schedule(new Task1(), 5,  
  10.                 TimeUnit.SECONDS);  
  11.         ScheduledFuture future2 = executor.schedule(new LongTask(), 3,  
  12.                 TimeUnit.SECONDS);  
  13.   
  14.         BlockingQueue<ScheduledFuture> blockingQueue = new ArrayBlockingQueue<ScheduledFuture>(  
  15.                 2true);  
  16.         blockingQueue.add(future2);  
  17.         blockingQueue.add(future1);  
  18.   
  19.         System.out.println(new Date());  
  20.         while (!blockingQueue.isEmpty()) {  
  21.             ScheduledFuture future = blockingQueue.poll();  
  22.             if (!future.isDone())  
  23.                 blockingQueue.add(future);  
  24.             else  
  25.                 System.out.println(future.get());  
  26.         }  
  27.         System.out.println(new Date());  
  28.         executor.shutdown();  
  29.     }  
  30.   
  31. }  

 首先,我们定义了一个ScheduledThreadPoolExecutor它的池长度是2。接着提交了两个任务:第一个任务将延迟5秒执行,第二个任务将延迟3秒执行。我们建立了一个BlockingQueue,用它来存储了ScheduledFuture,使用ScheduledFuture可以获得任务的执行结果。在一个while循环中,我们每次将一个ScheduledFuture从队列中弹出,验证它是否被执行,如果没有被执行则再次将它加入队列中,如果被执行了,这使用ScheduledFuture的get方法获取任务执行的结果。看一下执行结果:

Java代码    收藏代码
  1. Thu Apr 21 19:23:02 CST 2011  
  2. LongTask running: Thu Apr 21 19:23:05 CST 2011  
  3. Task1 running: Thu Apr 21 19:23:07 CST 2011  
  4. h1o2wd942e  
  5. success  
  6. Thu Apr 21 19:23:15 CST 2011  

 我们看到长任务先运行,因为长任务只等待了3秒,然后是输出字符串的任务运行,两个任务开始时间相差2秒,而先输出了字符串而后才是长任务运行的结果success,最后我们查看一下整体的开始和结束时间相差了13秒。

这说明在ScheduledThreadPoolExecutor中它不是以一个线程运行任务的,而是以多个线程,如果用一个线程运行任务,那么长任务运行完之前是不会运行输出字符串任务的。其实这个“多个任务“是我们自己指定的注意一下标红的代码,如果我们把这行代码改为:

Java代码    收藏代码
  1. ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);  

 那么运行结果就会发生变化,

Java代码    收藏代码
  1. Thu Apr 21 19:36:56 CST 2011  
  2. LongTask running: Thu Apr 21 19:36:59 CST 2011  
  3. success  
  4. Task1 running: Thu Apr 21 19:37:09 CST 2011  
  5. y981iqd0or  
  6. Thu Apr 21 19:37:09 CST 2011  

 这时其实和使用Timer运行结果是一样的。任务是在一个线程里顺序执行的。

 

2、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. public class ScheduledThreadPoolExec {  
  2.     public static void main(String[] args) throws InterruptedException,  
  3.             ExecutionException {  
  4.         ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(  
  5.                 2);  
  6.         ScheduledFuture future1 = executor.schedule(new Task1(), 5,  
  7.                 TimeUnit.SECONDS);  
  8.         ScheduledFuture future2 = executor.schedule(new LongTask(), 3,  
  9.                 TimeUnit.SECONDS);  
  10.         <strong><span style="color: #ff0000;">executor.schedule(new TimerExceptionTask(), 1, TimeUnit.SECONDS);  
  11.         executor.schedule(new TimerExceptionTask(), 2, TimeUnit.SECONDS);</span>  
  12. </strong>  
  13.   
  14.   
  15.   
  16.         BlockingQueue<ScheduledFuture> blockingQueue = new ArrayBlockingQueue<ScheduledFuture>(  
  17.                 2true);  
  18.         blockingQueue.add(future2);  
  19.         blockingQueue.add(future1);  
  20.   
  21.         System.out.println(new Date());  
  22.         while (!blockingQueue.isEmpty()) {  
  23.             ScheduledFuture future = blockingQueue.poll();  
  24.             if (!future.isDone())  
  25.                 blockingQueue.add(future);  
  26.             else  
  27.                 System.out.println(future.get());  
  28.         }  
  29.         System.out.println(new Date());  
  30.         executor.shutdown();  
  31.     }  
  32.   
  33. }  

 注意,标红的代码,如果这两个代码抛出错误后会影响后续任务,那么就应该在此终止,但是看一下结果,

Java代码    收藏代码
  1. Thu Apr 21 19:40:15 CST 2011  
  2. TimerExceptionTask: Thu Apr 21 19:40:16 CST 2011  
  3. TimerExceptionTask: Thu Apr 21 19:40:17 CST 2011  
  4. LongTask running: Thu Apr 21 19:40:18 CST 2011  
  5. Task1 running: Thu Apr 21 19:40:20 CST 2011  
  6. v5gcf01iiz  
  7. success  
  8. Thu Apr 21 19:40:28 CST 2011  

 后续任务仍然执行,可能会有朋友说:“你上面的池设置的是2,所以很有可能是那两个抛出异常的任务都在同一个线程中执行,而另一个线程执行了后续的任务”。那我们就把ScheduledThreadPoolExecutor设置成1看看,结果如下:

Java代码    收藏代码
  1. Thu Apr 21 19:43:00 CST 2011  
  2. TimerExceptionTask: Thu Apr 21 19:43:01 CST 2011  
  3. TimerExceptionTask: Thu Apr 21 19:43:02 CST 2011  
  4. LongTask running: Thu Apr 21 19:43:03 CST 2011  
  5. success  
  6. Task1 running: Thu Apr 21 19:43:13 CST 2011  
  7. 33kgv8onnd  
  8. Thu Apr 21 19:43:13 CST 2011  

后续任务也执行了,所以说ScheduledThreadPoolExecutor不会像Timer那样有线程泄漏现象。

对于周期性执行和Timer很类似这里就不再举例了。

 

你可能感兴趣的:(定时且周期性的任务研究I--Timer ---- 任务研究II--ScheduledThreadPoolExecutor)