jdk并發(fā)部分,如果英文理解有點小難,可以參考http://www.yiibai.com/java6/java/util/concurrent/package-summary.html
本篇轉(zhuǎn)自:http://victorzhzh.iteye.com/blog/1011635
很多時候我們希望任務(wù)可以定時的周期性的執(zhí)行,在最初的JAVA工具類庫中,通過Timer可以實現(xiàn)定時的周期性的需求,但是有一定的缺陷,例如:Timer是基于絕對時間的而非支持相對時間,因此Timer對系統(tǒng)時鐘比較敏感。雖然有一定的問題,但是我們還是從這個最簡單的實現(xiàn)開始研究。
首先,我們準(zhǔn)備一些討論問題的類: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());
- }
- }
這個類負(fù)責(zé)生成一個含有10個字符的字符串,這里我們將輸出時間打印出來,近似認(rèn)為是任務(wù)執(zhí)行的時間。
- 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: 已經(jīng)醒來");
- }
- }
這個類啟動了一個長任務(wù),即讓任務(wù)沉睡10秒。
下面我們來看一個定時任務(wù)執(zhí)行的例子:
- 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任務(wù),且讓它延遲1毫秒執(zhí)行,緊接著我們又提交了一個TimerLongTask長任務(wù),且讓它也延遲1毫秒執(zhí)行,最后我們在提交一個TimerTask1任務(wù),延遲2毫秒執(zhí)行。然后讓主線程沉睡20秒后關(guān)閉timer。我們看一下執(zhí)行結(jié)果:
- Thu Apr 21 11:04:31 CST 2011,utg3hn7u4r
- TimerLongTask: 開始沉睡
- TimerLongTask: 已經(jīng)醒來
- Thu Apr 21 11:04:41 CST 2011,4aac22sud1
這里我們看到第一次輸出10個字符的時間和第二次輸出10個字符的時間上相差了10秒,這10秒恰恰是長任務(wù)沉睡的時間,通過這個輸出我們可以分析出:Timer用來執(zhí)行任務(wù)的線程其實只有一個,且逐一被執(zhí)行。接下來我們查看一下源碼驗證一下,如下:
- private TaskQueue queue = new TaskQueue();
- private TimerThread thread = new TimerThread(queue);
這兩行代碼來自Timer源碼,我們可以看到在第一次創(chuàng)建了Timer時就已經(jīng)創(chuàng)建了一個thread和一個queue,因此只有一個線程來執(zhí)行我們的任務(wù)。
那么Timer是如何來執(zhí)行任務(wù)的?
首先我們調(diào)用timer.schedule方法,將任務(wù)提交到timer中,Timer中有很多重載的schedule方法,但它們都會調(diào)用同一個方法即sched方法。這個方法會將我們提交的任務(wù)添加到TaskQueue的隊列中(即queue),在每次添加時都會根據(jù)nextExecutionTime大小來調(diào)整隊列中任務(wù)的順序,讓nextExecutionTime最小的排在隊列的最前端,nextExecutionTime最大的排在隊列的最后端。在創(chuàng)建Timer時,我們同時也創(chuàng)建了一個TimerThread即thread,并且啟動了這個線程,
- public Timer(String name) {
- thread.setName(name);
- thread.start();
- }
TimerThread中的mainLoop方法是核心,它會完成所有的任務(wù)執(zhí)行,在一開始我們的隊列為空,這時mainLoop方法將會使線程進(jìn)入等待狀態(tài),當(dāng)我們使用schedule提交任務(wù)時會notify這個TimerThread線程,若任務(wù)的執(zhí)行未到則在wait相對的時間差。
我們調(diào)整一下上面的代碼,
- 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();
這樣先提交兩個輸出字符的任務(wù)最后提交長任務(wù),在這里,我們讓第二個輸出字符的任務(wù)延遲5秒執(zhí)行,長任務(wù)延遲3秒執(zhí)行,這樣得到的結(jié)果如下:
- Thu Apr 21 13:07:44 CST 2011,2sstwluvgc
- TimerLongTask: 開始沉睡
- TimerLongTask: 已經(jīng)醒來
- Thu Apr 21 13:07:57 CST 2011,sh4fnkqqc8
雖然我們改變了提交順序,但是還是按照延遲時間遞增排序執(zhí)行的,兩個輸出字符串的時間之間相差13秒,這也是長任務(wù)等待執(zhí)行時間+長任務(wù)睡眠時間之和。
重復(fù)執(zhí)行scheduleAtFixedRate方法提交任務(wù),主要是調(diào)用rescheduleMin方法對已經(jīng)調(diào)用的任務(wù)進(jìn)行重新設(shè)置調(diào)度延遲,并調(diào)用fixDown方法對隊列里的任務(wù)根據(jù)延遲時間重新排序。
- Timer timer = new Timer();
- timer.scheduleAtFixedRate(new TimerTask1(), 3000, 5000);
3000,代表第一次執(zhí)行時等待的時間,5000代表每次執(zhí)行任務(wù)之間的時間間隔,運(yùn)行結(jié)果:
- 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秒運(yùn)行一次。
由于Timer只使用一個線程運(yùn)行所有的任務(wù),那么當(dāng)一個任務(wù)拋出運(yùn)行時異常后會有什么樣的情形呢?其他的任務(wù)是否可以繼續(xù)?我們已經(jīng)有了前面的知識可以先猜想一個結(jié)果:因為Timer只使用一個線程運(yùn)行所有的任務(wù),所以當(dāng)一個線程拋出運(yùn)行時異常時,這個線程就基本掛了,不會在執(zhí)行后續(xù)的任何代碼,因此我們可以斷言,當(dāng)一個任務(wù)拋出運(yùn)行時異常時,后續(xù)任務(wù)都不可以執(zhí)行。為了證明這個猜想,我們需要一個可以拋出異常的任務(wù),如下:
- public class TimerExceptionTask extends TimerTask {
- @Override
- public void run() {
- System.out.println("TimerExceptionTask: "+new Date());
- throw new RuntimeException();
- }
- }
這個任務(wù)拋出一個運(yùn)行時異常。接著我們需要定義一下我們?nèi)蝿?wù)執(zhí)行的順序:先執(zhí)行一個正常的任務(wù),然后在執(zhí)行一個拋出異常的任務(wù),最后在執(zhí)行一個正常的任務(wù),如下:
- 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秒執(zhí)行正常輸出字符串的任務(wù),延遲3秒執(zhí)行拋出異常的任務(wù),延遲5秒執(zhí)行正常輸出字符串的任務(wù),看一下結(jié)果:
- 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)
并沒有輸出兩個字符串,只執(zhí)行了第一次的輸出字符串任務(wù),說明當(dāng)拋出運(yùn)行時異常時,其后續(xù)任務(wù)不可能被執(zhí)行。
鑒于Timer的缺陷,所以對它的使用還是要謹(jǐn)慎的,還好并發(fā)包中為我們提供了相應(yīng)的替代品:
我們將圍繞這些不足之處看看ScheduledThreadPoolExecutor是如何優(yōu)化的。
為了研究方便我們需要兩個類:
- 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接口目的是我們不再任務(wù)中直接輸出結(jié)果,而主動取獲取任務(wù)的結(jié)果
- 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";
- }
- }
長任務(wù)類,這里我們讓任務(wù)沉睡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。接著提交了兩個任務(wù):第一個任務(wù)將延遲5秒執(zhí)行,第二個任務(wù)將延遲3秒執(zhí)行。我們建立了一個BlockingQueue,用它來存儲了ScheduledFuture,使用ScheduledFuture可以獲得任務(wù)的執(zhí)行結(jié)果。在一個while循環(huán)中,我們每次將一個ScheduledFuture從隊列中彈出,驗證它是否被執(zhí)行,如果沒有被執(zhí)行則再次將它加入隊列中,如果被執(zhí)行了,這使用ScheduledFuture的get方法獲取任務(wù)執(zhí)行的結(jié)果。看一下執(zhí)行結(jié)果:
- 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
我們看到長任務(wù)先運(yùn)行,因為長任務(wù)只等待了3秒,然后是輸出字符串的任務(wù)運(yùn)行,兩個任務(wù)開始時間相差2秒,而先輸出了字符串而后才是長任務(wù)運(yùn)行的結(jié)果success,最后我們查看一下整體的開始和結(jié)束時間相差了13秒。
這說明在ScheduledThreadPoolExecutor中它不是以一個線程運(yùn)行任務(wù)的,而是以多個線程,如果用一個線程運(yùn)行任務(wù),那么長任務(wù)運(yùn)行完之前是不會運(yùn)行輸出字符串任務(wù)的。其實這個“多個任務(wù)“是我們自己指定的注意一下標(biāo)紅的代碼,如果我們把這行代碼改為:
- ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
那么運(yùn)行結(jié)果就會發(fā)生變化,
- 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運(yùn)行結(jié)果是一樣的。任務(wù)是在一個線程里順序執(zhí)行的。
2、Timer中一但有運(yùn)行時異常報出后續(xù)任務(wù)是否還會正常運(yùn)行?
為了研究這個問題,我們還是需要一個能夠拋出異常的任務(wù),如下:
- public class TimerExceptionTask extends TimerTask {
- @Override
- public void run() {
- System.out.println("TimerExceptionTask: "+new Date());
- throw new RuntimeException();
- }
- }
我們對上面運(yùn)行任務(wù)的代碼做一點點小小的修改,先運(yùn)行兩個拋出異常的任務(wù),如下:
- 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();
- }
- }
注意,標(biāo)紅的代碼,如果這兩個代碼拋出錯誤后會影響后續(xù)任務(wù),那么就應(yīng)該在此終止,但是看一下結(jié)果,
- 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
后續(xù)任務(wù)仍然執(zhí)行,可能會有朋友說:“你上面的池設(shè)置的是2,所以很有可能是那兩個拋出異常的任務(wù)都在同一個線程中執(zhí)行,而另一個線程執(zhí)行了后續(xù)的任務(wù)”。那我們就把ScheduledThreadPoolExecutor設(shè)置成1看看,結(jié)果如下:
- 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
后續(xù)任務(wù)也執(zhí)行了,所以說ScheduledThreadPoolExecutor不會像Timer那樣有線程泄漏現(xiàn)象。
對于周期性執(zhí)行和Timer很類似這里就不再舉例了。