Java多執行緒20:多執行緒下的其他元件之CyclicBarrier、Callable、Future和FutureTask
CyclicBarrier
接著講多執行緒下的其他元件,第一個要講的就是CyclicBarrier。CyclicBarrier從字面理解是指迴圈屏障,它可以協同多個執行緒,讓多個執行緒在這個屏障前等待,直到所有執行緒都達到了這個屏障時,再一起繼續執行後面的動作。看一下CyclicBarrier的使用例項:
public static class CyclicBarrierThread extends Thread { private CyclicBarrier cb; private int sleepSecond; public CyclicBarrierThread(CyclicBarrier cb, int sleepSecond) { this.cb = cb; this.sleepSecond = sleepSecond; } public void run() { try { System.out.println(this.getName() + "運行了"); Thread.sleep(sleepSecond * 1000); System.out.println(this.getName() + "準備等待了, 時間為" + System.currentTimeMillis()); cb.await(); System.out.println(this.getName() + "結束等待了, 時間為" + System.currentTimeMillis()); } catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) { Runnable runnable = new Runnable() { public void run() { System.out.println("CyclicBarrier的所有執行緒await()結束了,我運行了, 時間為" + System.currentTimeMillis()); } }; CyclicBarrier cb = new CyclicBarrier(3, runnable); CyclicBarrierThread cbt0 = new CyclicBarrierThread(cb, 3); CyclicBarrierThread cbt1 = new CyclicBarrierThread(cb, 6); CyclicBarrierThread cbt2 = new CyclicBarrierThread(cb, 9); cbt0.start(); cbt1.start(); cbt2.start(); }
看一下執行結果
Thread-0運行了
Thread-2運行了
Thread-1運行了
Thread-0準備等待了, 時間為1444650316313
Thread-1準備等待了, 時間為1444650319313
Thread-2準備等待了, 時間為1444650322313
CyclicBarrier的所有執行緒await()結束了,我運行了, 時間為1444650322313
Thread-2結束等待了, 時間為1444650322313
Thread-0結束等待了, 時間為1444650322313
Thread-1結束等待了, 時間為1444650322313
從執行結果看,由於是同一個CyclicBarrier,Thread-0先執行到了await()的地方,等著;Thread-2接著執行到了await()的地方,還等著;Thread-1最後執行到了await()的地方,所有的執行緒都執行到了await()的地方,所以三個執行緒以及指定的Runnable"同時"執行後面的程式碼,可以看到,await()之後,四個執行緒執行的時間一模一樣,都是1444650322313。
從使用來看,可能有人覺得CyclicBarrier和CountDownLatch有點像,都是多個執行緒等待相互完成之後,再執行後面的程式碼。實際上,CountDownLatch和CyclicBarrier都是用於多個執行緒間的協調的,它們二者的幾個差別是:
1、CountDownLatch是在多個執行緒都進行了latch.countDown()後才會觸發事件,喚醒await()在latch上的執行緒,而執行countDown()的執行緒,執行完countDown()後會繼續自己執行緒的工作;CyclicBarrier是一個柵欄,用於同步所有呼叫await()方法的執行緒,執行緒執行了await()方法之後並不會執行之後的程式碼,而只有當執行await()方法的執行緒數等於指定的parties之後,這些執行了await()方法的執行緒才會同時執行
2、CountDownLatch不能迴圈使用,計數器減為0就減為0了,不能被重置;CyclicBarrier提供了reset()方法,支援迴圈使用
3、CountDownLatch當呼叫countDown()方法的執行緒數等於指定的數量之後,可以喚起多條執行緒的任務;CyclicBarrier當執行await()方法的執行緒等於指定的數量之後,只能喚起一個BarrierAction
注意,因為使用CyclicBarrier的執行緒都會阻塞在await方法上,所以線上程池中使用CyclicBarrier時要特別小心,如果執行緒池的執行緒過少,那麼就會發生死鎖了
Callable、Future和FutureTask
Callable
Callable和Runnable差不多,兩者都是為那些其例項可能被另一個執行緒執行的類而設計的,最主要的差別在於Runnable不會返回執行緒運算結果,Callable可以(假如執行緒需要返回執行結果)
Future
Future是一個介面表示非同步計算的結果,它提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。Future提供了get()、cancel()、isCancel()、isDone()四種方法,表示Future有三種功能:
1、判斷任務是否完成
2、中斷任務
3、獲取任務執行結果
FutureTask
FutureTask是Future的實現類,它提供了對Future的基本實現。可使用FutureTask包裝Callable或Runnable物件,因為FutureTask實現了Runnable,所以也可以將FutureTask提交給Executor。
使用方法
Callable、Future、FutureTask一般都是和執行緒池配合使用的,因為執行緒池ThreadPoolExecutor的父類AbstractExecutorService提供了三種submit方法:
1、public Future<?> submit(Runnable task){...}
2、public <T> Future<T> submit<Runnable task, T result>{...}
3、public <T> Future<T> submit<Callable<T> task>{...}
第2個用得不多,第1個和第3個比較有用
Callable+Future使用示例
public static class CallableThread implements Callable<String>
{
public String call() throws Exception
{
System.out.println("進入CallableThread的call()方法, 開始睡覺, 睡覺時間為" + System.currentTimeMillis());
Thread.sleep(10000);
return "123";
}
}
public static void main(String[] args) throws Exception
{
ExecutorService es = Executors.newCachedThreadPool();
CallableThread ct = new CallableThread();
Future<String> f = es.submit(ct);
es.shutdown();
Thread.sleep(5000);
System.out.println("主執行緒等待5秒, 當前時間為" + System.currentTimeMillis());
String str = f.get();
System.out.println("Future已拿到資料, str = " + str + ", 當前時間為" + System.currentTimeMillis());
}
執行結果為:
進入CallableThread的call()方法, 開始睡覺, 睡覺時間為1444654421368
主執行緒等待5秒, 當前時間為1444654426369
Future已拿到資料, str = 123, 當前時間為1444654431369
看到任意一個利用Callable介面submit上去的任務,只要有一個Future接受它,Future便可以在程式任何地點嘗試去獲取這條執行緒返回出去的資料,時間可以比對一下,正好10000ms,即10s
Callable+FutureTask使用示例
有興趣的可以看下原始碼,其實使用Callable+Future的方式,es.submit(ct)方法返回的Future,底層實現new出來的是一個FutureTask。那麼,我們看一下Callable+FutureTask的方式:
public static class CallableThread implements Callable<String>
{
public String call() throws Exception
{
System.out.println("進入CallableThread的call()方法, 開始睡覺, 睡覺時間為" + System.currentTimeMillis());
Thread.sleep(10000);
return "123";
}
}
public static void main(String[] args) throws Exception
{
ExecutorService es = Executors.newCachedThreadPool();
CallableThread ct = new CallableThread();
FutureTask<String> f = new FutureTask<String>(ct);
es.submit(f);
es.shutdown();
Thread.sleep(5000);
System.out.println("主執行緒等待5秒, 當前時間為" + System.currentTimeMillis());
String str = f.get();
System.out.println("Future已拿到資料, str = " + str + ", 當前時間為" + System.currentTimeMillis());
}
看下執行結果:
進入CallableThread的call()方法, 開始睡覺, 睡覺時間為1444655049199
主執行緒等待5秒, 當前時間為1444655054200
Future已拿到資料, str = 123, 當前時間為1444655059200
和上面的寫法執行結果一樣,就不解釋了
使用Callable、Future和FutureTask的好處
上面演示了兩個例子,其實反映的是現實中一種情況,把上面的例子稍微擴充套件一下就是:
有一個method()方法,方法中執行方法A返回一個數據要10秒鐘,A方法後面的程式碼一共要執行20秒鐘,但是這20秒的程式碼中有10秒的方法並不依賴方法A的執行結果,有10秒鐘的程式碼依賴方法A的執行結果。此時若採用同步的方式,那麼勢必要先等待10秒鐘,等待方法A執行完畢,返回資料,再執行後面20秒的程式碼。
不得不說這是一種低效率的做法。有了Callable、Future和FutureTask,那麼:
1、先把A方法的內容放到Callable實現類的call()方法中
2、method()方法中,Callable實現類傳入Executor的submit方法中
3、執行後面方法中10秒不依賴方法A執行結果的程式碼
4、獲取方法A的執行結果,執行後面方法中10秒依賴方法A執行結果的程式碼
這樣程式碼執行效率一下子就提高了,程式不必卡在A方法處。
當然,也可以不用Callable,採用實現Runnable的方式,run()方法執行完了想個辦法給method()方法中的某個變數V賦個值就好了。但是我上一篇文章開頭就說了,之所以要用多執行緒元件,就是因為JDK幫我們很好地實現好了程式碼細節,讓開發者更多可以關注業務層的邏輯。如果使用Runnable的方式,那麼我們自己就要考慮很多細節,比如Runnable實現類的run()方法執行完畢給V賦值是否執行緒安全、10秒後如果A方法沒有執行完導致V還沒有值怎麼辦,何況JDK還給使用者提供了取消任務、判斷任務是否存在等方法。既然JDK已經幫我們考慮並實現這些細節了,在沒有有說服力的理由的情況下,我們為什麼還要自己寫run()方法的實現呢?