JAVA多線程高並發學習筆記(三)——Callable、Future和FutureTask


為什么要是用Callable和Future

Runnable的局限性

Executor采用Runnable作為基本的表達形式,雖然Runnable的run方法能夠寫入日志,寫入文件,寫入數據庫等操作,但是它不能返回一個值,或者拋出一個受檢查的異常,有些需要返回值的需求就不能滿足了。

能夠取消

Executor中的任務有四個狀態:創建,提交,開始和完成。如果說有些任務執行時間比較長,希望能夠取消該任務,Executor中的任務在未開始前是可以取消的,如果已經開始了,只能通過中斷的方式來取消。如果使用Callable和Future的結合,可以使用Future的canel方法取消任務,這樣就方便多了。

 

一個例子:

import java.util.concurrent.*;

public class Demo1 {

    public static void main(String args[]) throws Exception {
        ServiceTask task = new ServiceTask();
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<Integer> result = executor.submit(task);
        executor.shutdown();
        System.out.println("正在執行任務");
        Thread.sleep(1000);
        System.out.println("task運行結果為:" + result.get());
    }
}

class ServiceTask implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000);
        int result = 0;
        // 假設一個很龐大的計算
        for(int i=1;i<100;i++){
            for (int j=0;j<i;j++){
                result +=j;
            }
        }
        return result;
    }
}

看一下執行結果:

這個例子就是一個非常簡單的使用Callable和Futute的例子,ServiceTask類實現了Callable接口,並返回一個Integer類型的值。

Future<Integer> result = executor.submit(task);這行代碼就是構造一個Future。使用其get()方法就能得到最后的運行值。

 好了看完這一個簡單的例子,那就來仔細了解一下它們。

了解Callable和Future

Callable

 來看一下callable的代碼:

public abstract interface Callable<V> {
    public abstract V call() throws Exception;
}

可以看出它是接口,提到接口就可以明白接口是靈活的,支持傳入泛型參數。這個沒什么,我們來重點介紹一下Future

Future

首先來看關於它的介紹
Future提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。計算完成后只能使用 get 方法來獲取結果,如有必要,計算完成前可以阻塞此方法。取消則由 cancel 方法來執行。還提供了其他方法,以確定任務是正常完成還是被取消了。

來看一下Future的代碼

public abstract interface Future<V> {
    public abstract boolean cancel(boolean paramBoolean);

    public abstract boolean isCancelled();

    public abstract boolean isDone();

    public abstract V get() throws InterruptedException, ExecutionException;

    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
            throws InterruptedException, ExecutionException, TimeoutException;
}

提供了五個方法

 

public abstract boolean cancel(boolean paramBoolean)

試圖取消任務的執行(注意是試圖),因為存在一些任務已完成、已取消或者因為某些原因無法取消的因素,存在着取消失敗的可能性。

當canel方法起作用時,有兩個情況:

1.任務未開始,則該任務將永遠不會運行;

2.任務處於執行狀態,paramBoolean表示是否采用中斷的方式中斷線程。

 

public abstract boolean isCancelled()

如果任務正常取消的,則返回true。

 

 

public abstract boolean isDone();

如果任務已完成,則返回 true。 可能由於正常終止、異常或取消而完成,在所有這些情況中,此方法都將返回 true

(注意如果調用isCanle方法,那么isDone將始終返回true).

 

public abstract V get() throws InterruptedException, ExecutionException;

重點到了!這是Future獲取計算結果的方式之一,使用get方法。(注意這里返回的是Callable中的泛型)

 get方法取決於任務的狀態(未開始,運行中,已完成),如果任務已經完成,那么get會立即返回或者拋出一個異常;

如果任務沒有完成,那么get將阻塞知道任務完成。如果任務拋出了異常,那么get會將該異常封裝成ExecutionException拋出。

 

public abstract V get(long paramLong, TimeUnit paramTimeUnit) throws InterruptedException, ExecutionException, TimeoutException;

如果需要在給定時間后獲取計算結果,可以使用這個方法,如果超過給定時間之后沒有得到計算結果,則拋出TimeoutException。(注意這里返回的是Callable中的泛型)  

 

如何使用

來看代碼:

import java.util.concurrent.*;

public class Demo1 {

    public static void main(String args[]) throws Exception {
        // 1.先實例化任務對象
        ServiceTask task = new ServiceTask();
        // 2.實例化Executor框架中的線程池
        ExecutorService executor = Executors.newCachedThreadPool();
        // 3.使用submit方法將任務提交(返回的是一個Future)
        Future<Integer> result = executor.submit(task);
        // 4.記得關閉線程池
        executor.shutdown();
        System.out.println("正在執行任務");
        Thread.sleep(1000);
        // 5.打印最后的結果
        System.out.println("task運行結果為:" + result.get());
    }
}

/**
 * Callable的實現類
 */
class ServiceTask implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        Thread.sleep(2000);
        int result = 0;
        // 假設一個很龐大的計算
        for(int i=1;i<100;i++){
            for (int j=0;j<i;j++){
                result +=j;
            }
        }
        return result;
    }
}

運行結果:

接下來我們來試一下定時取結果:

還是在原來的代碼上修改:

import java.util.concurrent.*;

public class Demo1 {

    public static void main(String args[]) throws Exception {
        // 1.先實例化任務對象
        ServiceTask task = new ServiceTask();
        // 2.實例化Executor框架中的線程池
        ExecutorService executor = Executors.newCachedThreadPool();
        // 3.使用submit方法將任務提交(返回的是一個Future)
        Future<Integer> result = executor.submit(task);
        // 4.記得關閉線程池
        executor.shutdown();
        System.out.println("正在執行任務");
        Thread.sleep(1000);
        // 5.設置定時一秒取結果
        System.out.println("task運行結果為:" + result.get(1,TimeUnit.MILLISECONDS));
    }
}

/**
 * Callable的實現類
 */
class ServiceTask implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        //這里睡眠2秒
        Thread.sleep(2000);
        int result = 0;
        // 假設一個很龐大的計算
        for(int i=1;i<100;i++){
            for (int j=0;j<i;j++){
                result +=j;
            }
        }
        return result;
    }
}

來提前猜想一下,首先設置了定時一秒之后取得結果,但是ServiceTask設置兩秒的睡眠時間,理應取結果失敗,看一下運行結果:

是的,如果在規定時間內無法取到結果,就會返回TimeoutException。

 

談談FutureTask

FutureTask是Future的實現類,它繼承了RunnableFuture,RunnableFuture實際上繼承了Runnable和Future接口。

來看一下使用如何FutureTask:

import java.util.concurrent.*;

public class FutureCallDemo2 {

    public static void main(String args[])throws  Exception{
        // 1.先實例化任務對象
        FutureTaskService task = new FutureTaskService();
        // 2.實例化Executor框架中的線程池
        ExecutorService excutor = Executors.newCachedThreadPool();
        // 3.直接new一個FutureTask
        FutureTask<Long> result = new FutureTask<Long>(task);
        // 4.提交任務
        excutor.submit(result);
        // 5.關閉線程池
        excutor.shutdown();
        System.out.println("主線程正在執行任務");
        System.out.println("task運行結果為:" + result.get());
    }
}

/**
 * 繼承Callable接口
 */
class FutureTaskService implements Callable<Long> {

    @Override
    public Long call() throws Exception {
        Thread.sleep(3000);
        // 10的階乘
        long sum = 1;
        for (int i = 1; i <= 10; i++) {
            sum = sum * i;
        }
        return sum;
    }
}

用法的話其實差不多。

 

總結:

Future和Callable可以實現異構任務,但是有很多值得考慮的地方。

比如一個類使用了兩個任務,一個負責渲染頁面,一個負責下載圖像。

偽代碼如下:

//通過獲取圖像
List<ImageData>ImageDataList = future.get();
for(ImageData data:ImageDataList ){
  //渲染頁面
  renderPage(data);    
}

看似並行的執行任務,但是卻存在着問題。如果說下載圖像的速度遠小於渲染頁面的速度,那么最終的執行速度就和串行無異了。

所以只有當大量相互獨立且同構的任務可以進行並發處理時,才能體現出將任務分到多個任務中帶來的性能提升,考慮實際情況再選擇使用會帶來事半功倍的效果。

 

本文參考:

Java並發編程實戰


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM