jdk并發部分,如果英文理解有點小難,可以參考http://www.yiibai.com/java6/java/util/concurrent/package-summary.html
本篇轉自:http://victorzhzh.iteye.com/blog/1011635
很多時候我們希望任務可以定時的周期性的執行,在最初的JAVA工具類庫中,通過Timer可以實現定時的周期性的需求,但是有一定的缺陷,例如:Timer是基于絕對時間的而非支持相對時間,因此Timer對系統時鐘比較敏感。雖然有一定的問題,但是我們還是從這個最簡單的實現開始研究。
首先,我們準備一些討論問題的類:TimerTask1和TimerLongTask,如下
- public class TimerTask1 extends TimerTask {
-
- @Override
- public void run() {
- String base = "abcdefghijklmnopqrstuvwxyz0123456789";
- Random random = new Random();
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < 10; i++) {
- int number = random.nextInt(base.length());
- sb.append(base.charAt(number));
- }
- System.out.println(new Date()+","+sb.toString());
- }
-
- }
這個類負責生成一個含有10個字符的字符串,這里我們將輸出時間打印出來,近似認為是任務執行的時間。
- public class TimerLongTask extends TimerTask {
-
- @Override
- public void run() {
- System.out.println("TimerLongTask: 開始沉睡");
- try {
- TimeUnit.SECONDS.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("TimerLongTask: 已經醒來");
- }
-
- }
這個類啟動了一個長任務,即讓任務沉睡10秒。
下面我們來看一個定時任務執行的例子:
- public static void main(String[] args) throws InterruptedException {
- Timer timer = new Timer();
- timer.schedule(new TimerTask1(), 1);
- timer.schedule(new TimerLongTask(), 1);
- timer.schedule(new TimerTask1(), 2);
- TimeUnit.SECONDS.sleep(20);
- timer.cancel();
- }
在這個例子中,我們先提交了一個TimerTask1任務,且讓它延遲1毫秒執行,緊接著我們又提交了一個TimerLongTask長任務,且讓它也延遲1毫秒執行,最后我們在提交一個TimerTask1任務,延遲2毫秒執行。然后讓主線程沉睡20秒后關閉timer。我們看一下執行結果:
- Thu Apr 21 11:04:31 CST 2011,utg3hn7u4r
- TimerLongTask: 開始沉睡
- TimerLongTask: 已經醒來
- Thu Apr 21 11:04:41 CST 2011,4aac22sud1
這里我們看到第一次輸出10個字符的時間和第二次輸出10個字符的時間上相差了10秒,這10秒恰恰是長任務沉睡的時間,通過這個輸出我們可以分析出:Timer用來執行任務的線程其實只有一個,且逐一被執行。接下來我們查看一下源碼驗證一下,如下:
- private TaskQueue queue = new TaskQueue();
- 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,并且啟動了這個線程,
- public Timer(String name) {
- thread.setName(name);
- thread.start();
- }
TimerThread中的mainLoop方法是核心,它會完成所有的任務執行,在一開始我們的隊列為空,這時mainLoop方法將會使線程進入等待狀態,當我們使用schedule提交任務時會notify這個TimerThread線程,若任務的執行未到則在wait相對的時間差。
我們調整一下上面的代碼,
- Timer timer = new Timer();
- timer.schedule(new TimerTask1(), 1);
- timer.schedule(new TimerTask1(), 5000);
- timer.schedule(new TimerLongTask(), 3000);
- TimeUnit.SECONDS.sleep(20);
- timer.cancel();
這樣先提交兩個輸出字符的任務最后提交長任務,在這里,我們讓第二個輸出字符的任務延遲5秒執行,長任務延遲3秒執行,這樣得到的結果如下:
- Thu Apr 21 13:07:44 CST 2011,2sstwluvgc
- TimerLongTask: 開始沉睡
- TimerLongTask: 已經醒來
- Thu Apr 21 13:07:57 CST 2011,sh4fnkqqc8
雖然我們改變了提交順序,但是還是按照延遲時間遞增排序執行的,兩個輸出字符串的時間之間相差13秒,這也是長任務等待執行時間+長任務睡眠時間之和。
重復執行scheduleAtFixedRate方法提交任務,主要是調用rescheduleMin方法對已經調用的任務進行重新設置調度延遲,并調用fixDown方法對隊列里的任務根據延遲時間重新排序。
- Timer timer = new Timer();
- timer.scheduleAtFixedRate(new TimerTask1(), 3000, 5000);
3000,代表第一次執行時等待的時間,5000代表每次執行任務之間的時間間隔,運行結果:
- Thu Apr 21 13:14:55 CST 2011,izf536esrg
- Thu Apr 21 13:15:00 CST 2011,2khzm7e09v
- Thu Apr 21 13:15:05 CST 2011,jc3dvt2m8q
基本是每5秒運行一次。
由于Timer只使用一個線程運行所有的任務,那么當一個任務拋出運行時異常后會有什么樣的情形呢?其他的任務是否可以繼續?我們已經有了前面的知識可以先猜想一個結果:因為Timer只使用一個線程運行所有的任務,所以當一個線程拋出運行時異常時,這個線程就基本掛了,不會在執行后續的任何代碼,因此我們可以斷言,當一個任務拋出運行時異常時,后續任務都不可以執行。為了證明這個猜想,我們需要一個可以拋出異常的任務,如下:
- public class TimerExceptionTask extends TimerTask {
-
- @Override
- public void run() {
- System.out.println("TimerExceptionTask: "+new Date());
- throw new RuntimeException();
- }
-
- }
這個任務拋出一個運行時異常。接著我們需要定義一下我們任務執行的順序:先執行一個正常的任務,然后在執行一個拋出異常的任務,最后在執行一個正常的任務,如下:
- Timer timer = new Timer();
- timer.schedule(new TimerTask1(), 1000);
- timer.schedule(new TimerExceptionTask(), 3000);
- timer.schedule(new TimerTask1(), 5000);
- TimeUnit.SECONDS.sleep(6);
- timer.cancel();
延遲1秒執行正常輸出字符串的任務,延遲3秒執行拋出異常的任務,延遲5秒執行正常輸出字符串的任務,看一下結果:
- Thu Apr 21 13:40:23 CST 2011,lk7fjneyyu
- TimerExceptionTask: Thu Apr 21 13:40:25 CST 2011
- Exception in thread "Timer-0" java.lang.RuntimeException
- at org.victorzhzh.concurrency.TimerExceptionTask.run(TimerExceptionTask.java:11)
- at java.util.TimerThread.mainLoop(Timer.java:512)
- at java.util.TimerThread.run(Timer.java:462)
并沒有輸出兩個字符串,只執行了第一次的輸出字符串任務,說明當拋出運行時異常時,其后續任務不可能被執行。
鑒于Timer的缺陷,所以對它的使用還是要謹慎的,還好并發包中為我們提供了相應的替代品:
我們將圍繞這些不足之處看看ScheduledThreadPoolExecutor是如何優化的。
為了研究方便我們需要兩個類:
- public class Task1 implements Callable<String> {
-
- @Override
- public String call() throws Exception {
- String base = "abcdefghijklmnopqrstuvwxyz0123456789";
- Random random = new Random();
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < 10; i++) {
- int number = random.nextInt(base.length());
- sb.append(base.charAt(number));
- }
- System.out.println("Task1 running: " + new Date());
- return sb.toString();
- }
-
- }
生成含有10個字符的字符串,使用Callable接口目的是我們不再任務中直接輸出結果,而主動取獲取任務的結果
- public class LongTask implements Callable<String> {
-
- @Override
- public String call() throws Exception {
- System.out.println("LongTask running: "+new Date());
- TimeUnit.SECONDS.sleep(10);
- return "success";
- }
-
- }
長任務類,這里我們讓任務沉睡10秒然后返回一個“success”
下面我們來分析一下ScheduledThreadPoolExecutor:
1、Timer中單線程問題是否在ScheduledThreadPoolExecutor中存在?
我們先來看一下面的程序:
- public class ScheduledThreadPoolExec {
- public static void main(String[] args) throws InterruptedException,
- ExecutionException {
- <strong><span style="color: #ff0000;">ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
- 2);</span>
- </strong>
-
-
- ScheduledFuture future1 = executor.schedule(new Task1(), 5,
- TimeUnit.SECONDS);
- ScheduledFuture future2 = executor.schedule(new LongTask(), 3,
- TimeUnit.SECONDS);
-
- BlockingQueue<ScheduledFuture> blockingQueue = new ArrayBlockingQueue<ScheduledFuture>(
- 2, true);
- blockingQueue.add(future2);
- blockingQueue.add(future1);
-
- System.out.println(new Date());
- while (!blockingQueue.isEmpty()) {
- ScheduledFuture future = blockingQueue.poll();
- if (!future.isDone())
- blockingQueue.add(future);
- else
- System.out.println(future.get());
- }
- System.out.println(new Date());
- executor.shutdown();
- }
-
- }
首先,我們定義了一個ScheduledThreadPoolExecutor它的池長度是2。接著提交了兩個任務:第一個任務將延遲5秒執行,第二個任務將延遲3秒執行。我們建立了一個BlockingQueue,用它來存儲了ScheduledFuture,使用ScheduledFuture可以獲得任務的執行結果。在一個while循環中,我們每次將一個ScheduledFuture從隊列中彈出,驗證它是否被執行,如果沒有被執行則再次將它加入隊列中,如果被執行了,這使用ScheduledFuture的get方法獲取任務執行的結果。看一下執行結果:
- Thu Apr 21 19:23:02 CST 2011
- LongTask running: Thu Apr 21 19:23:05 CST 2011
- Task1 running: Thu Apr 21 19:23:07 CST 2011
- h1o2wd942e
- success
- Thu Apr 21 19:23:15 CST 2011
我們看到長任務先運行,因為長任務只等待了3秒,然后是輸出字符串的任務運行,兩個任務開始時間相差2秒,而先輸出了字符串而后才是長任務運行的結果success,最后我們查看一下整體的開始和結束時間相差了13秒。
這說明在ScheduledThreadPoolExecutor中它不是以一個線程運行任務的,而是以多個線程,如果用一個線程運行任務,那么長任務運行完之前是不會運行輸出字符串任務的。其實這個“多個任務“是我們自己指定的注意一下標紅的代碼,如果我們把這行代碼改為:
- ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
那么運行結果就會發生變化,
- Thu Apr 21 19:36:56 CST 2011
- LongTask running: Thu Apr 21 19:36:59 CST 2011
- success
- Task1 running: Thu Apr 21 19:37:09 CST 2011
- y981iqd0or
- Thu Apr 21 19:37:09 CST 2011
這時其實和使用Timer運行結果是一樣的。任務是在一個線程里順序執行的。
2、Timer中一但有運行時異常報出后續任務是否還會正常運行?
為了研究這個問題,我們還是需要一個能夠拋出異常的任務,如下:
- public class TimerExceptionTask extends TimerTask {
-
- @Override
- public void run() {
- System.out.println("TimerExceptionTask: "+new Date());
- throw new RuntimeException();
- }
-
- }
我們對上面運行任務的代碼做一點點小小的修改,先運行兩個拋出異常的任務,如下:
- public class ScheduledThreadPoolExec {
- public static void main(String[] args) throws InterruptedException,
- ExecutionException {
- ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
- 2);
- ScheduledFuture future1 = executor.schedule(new Task1(), 5,
- TimeUnit.SECONDS);
- ScheduledFuture future2 = executor.schedule(new LongTask(), 3,
- TimeUnit.SECONDS);
- <strong><span style="color: #ff0000;">executor.schedule(new TimerExceptionTask(), 1, TimeUnit.SECONDS);
- executor.schedule(new TimerExceptionTask(), 2, TimeUnit.SECONDS);</span>
- </strong>
-
-
-
- BlockingQueue<ScheduledFuture> blockingQueue = new ArrayBlockingQueue<ScheduledFuture>(
- 2, true);
- blockingQueue.add(future2);
- blockingQueue.add(future1);
-
- System.out.println(new Date());
- while (!blockingQueue.isEmpty()) {
- ScheduledFuture future = blockingQueue.poll();
- if (!future.isDone())
- blockingQueue.add(future);
- else
- System.out.println(future.get());
- }
- System.out.println(new Date());
- executor.shutdown();
- }
-
- }
注意,標紅的代碼,如果這兩個代碼拋出錯誤后會影響后續任務,那么就應該在此終止,但是看一下結果,
- Thu Apr 21 19:40:15 CST 2011
- TimerExceptionTask: Thu Apr 21 19:40:16 CST 2011
- TimerExceptionTask: Thu Apr 21 19:40:17 CST 2011
- LongTask running: Thu Apr 21 19:40:18 CST 2011
- Task1 running: Thu Apr 21 19:40:20 CST 2011
- v5gcf01iiz
- success
- Thu Apr 21 19:40:28 CST 2011
后續任務仍然執行,可能會有朋友說:“你上面的池設置的是2,所以很有可能是那兩個拋出異常的任務都在同一個線程中執行,而另一個線程執行了后續的任務”。那我們就把ScheduledThreadPoolExecutor設置成1看看,結果如下:
- Thu Apr 21 19:43:00 CST 2011
- TimerExceptionTask: Thu Apr 21 19:43:01 CST 2011
- TimerExceptionTask: Thu Apr 21 19:43:02 CST 2011
- LongTask running: Thu Apr 21 19:43:03 CST 2011
- success
- Task1 running: Thu Apr 21 19:43:13 CST 2011
- 33kgv8onnd
- Thu Apr 21 19:43:13 CST 2011
后續任務也執行了,所以說ScheduledThreadPoolExecutor不會像Timer那樣有線程泄漏現象。
對于周期性執行和Timer很類似這里就不再舉例了。
posted on 2013-03-18 18:58
David1228 閱讀(1768)
評論(0) 編輯 收藏 所屬分類:
JAVA 、
線程-模式-設計