天天看點

Java多線程21:多線程下的其他元件之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<?> subit(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()方法的實作呢? 

==================================================================================

我不能保證寫的每個地方都是對的,但是至少能保證不複制、不黏貼,保證每一句話、每一行代碼都經過了認真的推敲、仔細的斟酌。每一篇文章的背後,希望都能看到自己對于技術、對于生活的态度。

我相信喬布斯說的,隻有那些瘋狂到認為自己可以改變世界的人才能真正地改變世界。面對壓力,我可以挑燈夜戰、不眠不休;面對困難,我願意迎難而上、永不退縮。

其實我想說的是,我隻是一個程式員,這就是我現在純粹人生的全部。