1. 程式人生 > >Android併發程式設計之白話文詳解Future,FutureTask和Callable

Android併發程式設計之白話文詳解Future,FutureTask和Callable

從最簡單的說起Thread和Runnable

說到併發程式設計,就一定是多個執行緒併發執行任務。那麼併發程式設計的基礎是什麼呢?沒錯那就是Thread了。一個Thread可以執行一個Runnable型別的物件。那麼Runnable是什麼呢?其實Runnable是一個介面,他只定義了一個方法run(),這個run()方法裡就是我們要執行的任務,並且是要被Thread呼叫的。因此,一個Runnable就可以理解為一個要被執行的任務,而Thread就是一個執行任務的工人!
接下來我們用一個例子來實踐一下,我們有一個任務(Runnable),這個任務就是來計算1+2+3+…+10,然後我們叫來一個工人(Thread)來執行這個任務。

public class ThreadDemo {
    public static void main(String[] args) {
        Thread worker = new Thread(new CountRunnable());
        worker.start();
    }
    public static class CountRunnable implements Runnable{
        private int sum;
        @Override
        public void run() {
            for
(int i=1 ; i<11 ; i++){ sum = sum+i; } System.out.println("sum="+sum); } } }

這裡寫圖片描述
這裡我們呼叫了Thread的start()方法,相當於通知我們的工人去幹活,然後工人Thread去呼叫任務Runnable的run()方法去幹活。
這裡寫圖片描述

如果我們覺得一個工人不夠用,那麼我們可以多叫來幾個工人,讓他們一起來工作,這就是併發程式設計了!

public class ThreadDemo {
    public
static void main(String[] args) throws Exception{ List<Thread> threads = new ArrayList<Thread>(); for(int i=1 ; i<101 ; i++){ Thread thread = new Thread(new CountRunnable(),"Thread"+i); threads.add(thread); thread.start(); } for(Thread t : threads){ t.join(); } System.out.println("所有執行緒執行完畢!"); } public static class CountRunnable implements Runnable{ private int sum; @Override public void run() { for(int i=1 ; i<11 ; i++){ sum = sum+i; } System.out.println(Thread.currentThread().getName()+"執行完畢 sum="+sum); } } }

這裡寫圖片描述
我們看到,每個sum都等於55,不是聽說多執行緒併發執行會帶來執行緒不安全的問題嗎?其實這裡我們是給每個工人分配一個只屬於自己的任務,每個工人幹自己的活,所以並不會影響到其他的人

Thread thread = new Thread(new CountRunnable(),"Thread"+i);

那麼什麼情況下會出現執行緒併發的問題的?我們要做的就是把一個任務同時分配給100名工人,那麼就會出現執行緒不安全的問題

public class ThreadDemo {
    public static void main(String[] args) throws Exception{
        List<Thread> threads = new ArrayList<Thread>();
        //注意這裡,我們在外面new出一個任務來,讓100個執行緒都來執行這個任務
        CountRunnable work = new CountRunnable();
        for(int i=1 ; i<101; i++){
            Thread thread = new Thread(work,"Thread"+i);
            threads.add(thread);
        }
        for(Thread t : threads){
            t.start();
        }
        for(Thread t : threads){
            t.join();
        }
        System.out.println("所有執行緒執行完畢");

    }
    public static class CountRunnable implements Runnable{
        private int sum;
        @Override
        public void run() {
            for(int i=1 ; i<11 ; i++){
                try {
                    //在這裡我們讓每個工人每進行一次加法運算後就休息1ms,這樣會使得結果明顯
                    TimeUnit.MILLISECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sum = sum+i;
            }
            System.out.println(Thread.currentThread().getName()+"執行完畢 sum="+sum);
        }

    }
}

這裡寫圖片描述
我們看到的結果簡直不堪入目,正確結果應該是5500對吧,這時因為我們有100名工人去幹一件任務,他們都操作的是同一個變數,因此多執行緒修改共享變數會出現問題,至於原理是什麼,大家可以參考我的另一篇文章Java併發程式設計之圖文解析volatile關鍵字來了解一下Java的記憶體模型(JMM)。

這篇文章的題目不是叫Future,FutureTask和Callable嗎?我怎麼連他們的影子都還沒有見到?大家先彆著急,還是和我一起慢慢深入,這樣才能真正理解他們存在的道理。

最簡單的方法出現了問題,執行緒池來解決

現在我們可以叫來這100名工人來為我們幹活了,可是這樣有個問題,這100名工人不是說找就找的,首先你得去發招聘啟事,接著再去面試,再去培訓等等,非常的費時費力,所以我們應該找到一個外包公司,比如我們需要100名工人,我們直接就到外包公司去借100名工人,直接來幹活,這樣就省了不少的力氣了,這個外包公司就是執行緒池了。關於執行緒池的介紹,有一篇寫的非常詳細的部落格Android效能優化之使用執行緒池處理非同步任務,既然已經有人把它的理論總結的很清晰透徹了,我就不再重複去介紹一遍了,如果大家有對執行緒池的基礎還不瞭解的話,推薦看看這篇文章。下面我就來說說執行緒池的使用,慢慢引出Future和Callable。

我們在使用執行緒池的時候,可以把一個任務(Runnable)交給執行緒池,呼叫執行緒池的execute(runnable)來執行

public class ExecutorDemo {
    public static void main(String[] args) {
        ExecutorService es = Executors.newSingleThreadExecutor();
        CountRunnable work = new CountRunnable();
        es.execute(work);
        es.shutdown();
        System.out.println("任務結束"+es.isShutdown());
    }
    public static class CountRunnable implements Runnable{
        private int sum;
        @Override
        public void run() {
            for(int i=1 ; i<11 ; i++){
                sum+=i;
            }
            System.out.println("sum="+sum);
        }
    }
}

這裡寫圖片描述

不知道大家有沒有發現一個問題,我們執行Runnable任務,他的run()方法是沒有返回值的,那如果我們想要執行完一個任務,並且能夠拿到一個返回值結果,那麼應該怎麼做呢?

Future登場

噹噹噹!沒錯!主角就要登場了!首先介紹Future

public interface Future<V> {


    boolean cancel(boolean mayInterruptIfRunning);


    boolean isCancelled();


    boolean isDone();


    V get() throws InterruptedException, ExecutionException;


    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future是一個介面,他提供給了我們方法來檢測當前的任務是否已經結束,還可以等待任務結束並且拿到一個結果,通過呼叫Future的get()方法可以當任務結束後返回一個結果值,如果工作沒有結束,則會阻塞當前執行緒,直到任務執行完畢,我們可以通過呼叫cancel()方法來停止一個任務,如果任務已經停止,則cancel()方法會返回true;如果任務已經完成或者已經停止了或者這個任務無法停止,則cancel()會返回一個false。當一個任務被成功停止後,他無法再次執行。isDone()和isCancel()方法可以判斷當前工作是否完成和是否取消。

簡單介紹一番,我們發現原來那些工人,只會去執行工作,做完工作之後也不給我們反饋資訊,並且我們也不知道他們何時能完工,更不能打斷他們的工作,這種工人的弊端就顯現出來了。

現在我們有了更高階的工人,這些工人只能是從外包公司來借(利用執行緒池),當這些工人幹完活之後,他們會給我們返回執行的結果,而且我們還可以暫停他們的工作。

我們看到執行緒池還有一個方法可以執行一個任務,那就是submit()方法

 public Future<?> submit(Runnable task) {
            return e.submit(task);
        }

我們看到他會返回一個Future物件,這個Future物件的泛型裡還用的是一個問號“?”,問號就是說我們不知道要返回的物件是什麼型別,那麼就返回一個null好了,因為我們執行的是一個Runnable物件,Runnable是沒有返回值的,所以這裡用一個問號,說明沒有返回值,那麼就返回一個null好了。

public class ExecutorDemo {
    public static void main(String[] args) {
        ExecutorService es = Executors.newSingleThreadExecutor();
        CountRunnable work = new CountRunnable();
        Future<?> future = es.submit(work);
        System.out.println("任務開始於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        for(int i=0 ; i<10 ; i++){
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("主執行緒"+Thread.currentThread().getName()+"仍然可以執行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        try {
            Object object = future.get();
            System.out.println("任務結束於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"  result="+object);
        } catch (Exception e) {
            e.printStackTrace();
        }
        es.shutdown();

        System.out.println("關閉執行緒池"+es.isShutdown());
    }
    public static class CountRunnable implements Runnable{
        private int sum;
        @Override
        public void run() {
            for(int i=1 ; i<11 ; i++){
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                    sum+=i;
                    System.out.println("工作執行緒"+Thread.currentThread().getName()+"正在執行  sum="+sum);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }
    }
}

這裡寫圖片描述
我們看到Future有一個get()方法,這個方法是一個阻塞方法,我們呼叫submit()執行一個任務的時候,會執行Runnable中的run()方法,當run()方法沒有執行完的時候,這個工人就會歇著了,直到run()方法執行結束後,工人就會立即將結果取回並且交給我們。我們看到返回的result=null。那既然返回null的話還有什麼意義呢??彆著急,那就要用到Callable介面了

Callable登場

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

我們看到Callable介面和Runnable介面很像,也是隻有一個方法,不過這個call()方法是有返回值的,這個返回值是一個泛型,也就是說我們可以根據我們的需求來指定我們要返回的result的型別

public class CallableDemo {
    public static void main(String[] args) throws Exception{
        ExecutorService es = Executors.newSingleThreadExecutor();
        Future<Number> future = es.submit(new CountCallable());
        System.out.println("任務開始於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        Number number = future.get();
        System.out.println("任務結束於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        if(future.isDone()){
            System.out.println("任務執行完畢  result="+number.num);
            es.shutdown();
        }

    }
    public static class CountCallable implements Callable<Number>{

        @Override
        public Number call() throws Exception {
            Number number = new Number();
            TimeUnit.SECONDS.sleep(2);
            number.setNum(10);
            return number;
        }

    }
    static class Number{
        private int num;
        private int getNum(){
            return num;
        }
        private void setNum(int num){
            this.num = num;
        }
    }
}

我們建立我們的任務(Callable)的時候,傳入了一個Number類的泛型,那麼在call()方法中就會返回這個Number型別的物件,最後在Future的get()方法中就會返回我們的Number型別的結果。
這裡寫圖片描述

然而Future不僅僅可以獲得一個結果,他還可以被取消,我們通過呼叫future的cancel()方法,可以取消一個Future的執行

public class CallableDemo {
    public static void main(String[] args) throws Exception{
        ExecutorService es = Executors.newSingleThreadExecutor();
        Future<Number> future = es.submit(new CountCallable());
        System.out.println("任務開始於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        future.cancel(true);
        if (future.isCancelled()) {
            System.out.println("任務被取消於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            es.shutdownNow();
        }else{
            Number number = future.get();
            System.out.println("任務結束於"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            if(future.isDone()){
                System.out.println("任務執行完畢  result="+number.num);
                es.shutdown();
            }
        }
    }

    public static class CountCallable implements Callable<Number>{

        @Override
        public Number call() throws Exception {
            Number number = new Number();
            TimeUnit.SECONDS.sleep(5);
            number.setNum(10);
            return number;
        }

    }
    static class Number{
        private int num;
        private int getNum(){
            return num;
        }
        private void setNum(int num){
            this.num = num;
        }
    }
}

這裡寫圖片描述

FutureTask登場

說完了Future和Callable,我們再來說最後一個FutureTask,Future是一個介面,他的唯一實現類就是FutureTask,其實FutureTask的一個很好地特點是他有一個回撥函式done()方法,當一個任務執行結束後,會回撥這個done()方法,我們可以在done()方法中呼叫FutureTask的get()方法來獲得計算的結果。為什麼我們要在done()方法中去呼叫get()方法呢? 這是有原因的,我在Android開發中,如果我在主執行緒去呼叫futureTask.get()方法時,會阻塞我的UI執行緒,如果在done()方法裡呼叫get(),則不會阻塞我們的UI執行緒。

public class FutureTask<V> implements RunnableFuture<V> {
}

我們來看看FutureTask實現了RunnableFuture介面

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

Runnable介面又實現了Runnable和Future介面,所以說FutureTask可以交給Executor執行,也可以由呼叫執行緒直接執行FutureTask.run()方法。FutureTask的run()方法中又會呼叫Callable的call()方法

public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

所以一個FutureTask實際上執行的是一個Callable型別例項的call()方法,call()方法才是我們的最終任務。其實Android中的AsyncTask內部也是使用的FutureTask,我們寫一個小的例子來模仿AsyncTask的可以停止的功能

public class MainActivity extends AppCompatActivity {
    FutureTask<Number> futureTask;
    CountCallable countCallable;
    ExecutorService es;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        countCallable = new CountCallable();
        futureTask = new FutureTask<Number>(countCallable){
            @Override
            protected void done() {
                try {
                    Number number = futureTask.get();
                    Log.i("zhangqi", "任務結束於" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()) + "  result=" + number.getNum());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                } catch (CancellationException e) {
                    Log.i("zhangqi", "任務被取消於" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));
                }
            }
        };
        es = Executors.newFixedThreadPool(2);
        es.execute(futureTask);
        Log.i("zhangqi", "任務被開始於" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));

    }



    public void cancel(View view) {
        futureTask.cancel(true);
    }

    public static class CountCallable implements Callable<Number> {

        @Override
        public Number call() throws Exception {
            Number number = new Number();
            Log.i("zhangqi","執行在"+Thread.currentThread().getName());
            Thread.sleep(5000);
            number.setNum(10);
            return number;
        }

    }

    static class Number {
        private int num;

        private int getNum() {
            return num;
        }

        private void setNum(int num) {
            this.num = num;
        }
    }

}

我們寫了一個CountCallable類,在call()方法裡是我們要執行的任務,最終我們的任務要返回一個Number型別的物件,我們在call()方法中首先會讓執行緒睡眠5秒鐘,然後new出一個Number物件並且給他賦值為10.

接著我們new一個FutureTask並且把我們的CountCallable物件傳入進去,FutureTask的泛型就是我們要返回的結果的型別,並且我們要重寫FutureTask的done()方法,這個方法會在任務結束後自動執行,在done()方法中我們呼叫get()方法獲得執行的結果
這裡寫圖片描述
這裡寫圖片描述

現在我們執行cancel方法,來結束這個FutureTask任務
這裡寫圖片描述
這裡寫圖片描述
在任務執行2秒的時候,我點選了cancel按鈕,執行了FutureTask的cancel方法,當我們執行了cancel()方法後,FutureTask的get()方法會丟擲CancellationException異常,我們捕捉這個異常,然後在這裡來處理一些後事 =-=!

相信大家都已經理解了為什麼Java要提供給我們Future,FutureTask和Callable了,他們其實是併發程式設計中更高階的應用,我們應該理解他們並且正確的使用它們。