【併發編程】 --- Runnable、Callable、Future和FutureTask之間的關係

源碼地址:https://github.com/nieandsun/concurrent-study.git


1 創建一個線程的方式到底有幾種???

在java中其實創建一個線程的方式本質上只有兩種:

  • (1)繼承Thread類
  • (2)實現Runnable接口

這個觀點可以通過Thread類中的註釋進行證明:
在這裏插入圖片描述
但是同時相信很多人也都知道,使用下面兩種組合,也可以創建一個線程:

  • (1)FutureTask + Callable
  • (2)線程池+Future+Callable

這時我想有些人可能會懵逼,難道JDK源碼中的註釋還有問題??? —> 其實並不是。


2 如何使用FutureTask 、Future、Callable、線程池實現線程


2.1 FutureTask + Callable實現多線程

  • code
package com.nrsc.ch2.future;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

@Slf4j
public class FutureTaskDemo1 {

    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int num = 0;
            log.info("開始進行計算。。。");
            for (int i = 0; i < 5; i++) {
                Thread.sleep(1000);
                num++;
            }
            return num;
        }
    }


    public static void main(String[] args) throws Exception {

        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        
        new Thread(futureTask).start(); //-----//此方法如果連續調用兩次,只會走一次
       
        Thread.sleep(1000);
        log.info("do something in main");

        //在Callable中的方法運行完之前 這裏會一直阻塞,直到Callable運行完
        //然後就可以從future裏獲取到結果了
        Integer result = futureTask.get();
        log.info("result: {}", result);
    }
}
  • 運行結果

在這裏插入圖片描述
注意事項:

  • 由於futureTask.get()會阻塞線程,所以通常的做法是
    • 將futureTask對應的方法最先執行 —> 即最先start
    • 將futureTask.get()放在主線程的最後 —> 防止主線程中的其他方法被阻塞
  • new Thread(futureTask).start();如果連續調用兩次只會走一次
    • 這個從常理上來說很好理解,即你沒必要執行兩次,因爲執行一次就可以獲得到想要的結果了
    • 當然如果你比較執拗,或者又真的有需求,那你得多定義一個futureTask,這樣兩個線程接受兩個不同的futureTask,就可以執行兩次了
    • 具體的底層原理不算很難,有興趣的可以擼一擼源碼

2.2 線程池+Future+Callable 實現多線程

  • code
package com.nrsc.ch2.future;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

@Slf4j
public class FutureDemo1 {

    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int num = 0;
            log.info("開始進行計算。。。");
            for (int i = 0; i < 5; i++) {
                Thread.sleep(1000);
                num++;
            }
            return num;
        }
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //線程池通過submit開啓線程
        Future<Integer> future = executorService.submit(new MyCallable());

        Thread.sleep(1000);

        log.info("do something in main");

        //在Callable中的方法運行完之前 這裏會一直阻塞,直到Callable運行完
        //然後就可以從future裏獲取到結果了
        Integer result = future.get();
        log.info("result: {}", result);
        executorService.shutdown();
    }
}
  • 運行結果,和2.1基本一致:

在這裏插入圖片描述


3 Runnable、Callable、Future和FutureTask之間的關係

3.1 整體關係介紹

看過2中的兩個栗子,我想你肯定也已經知道揭開這幾個對象之間關係的突破口就是FutureTask
爲什麼這麼說呢? 首先我們應該知道Thread的構造函數就下面這麼幾個。並且在1中也已經說過,Java中開啓線程的方式要麼是繼承Thread類,要麼是使用繼承了Runnable接口的類。但是在2.1中我卻在創建Thread類時傳入了一個FutureTask ----》 那這說明這個FutureTask 肯定就是一個Runnable(多態)。
在這裏插入圖片描述
接下來我們來看一下FutureTask類的繼承關係圖:
在這裏插入圖片描述
由此可知FutureTask確實是Runnable接口的子類,同時它還實現了Future接口。


3.2 簡單看一下源碼

  • Runnable接口源碼:
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • RunnableFuture接口源碼:
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}
  • 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;
}
  • Callable的源碼:
@FunctionalInterface
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;
}

接着再來看一下FutureTask中的方法
在這裏插入圖片描述


3.3 四者關係小結

通過前面的內容可知:

  • (1)JDK允許的創建線程的方式只有兩種,而這兩種方式都無法獲取線程中任務運行結果的返回值
  • (2)爲了想辦法獲取到各個線程的執行結果,在沒辦法改變創建線程方式的前提下
    • (2.1)出現了FutureTask,一種特殊的Runnable
    • (2.2)我們的線程開啓後其實還是會走沒有返回值的run()方法 —》 其實就是實現Runnable接口開啓線程的那種方式
    • (2.3)但是在沒有返回值的run()方法內部,可以調用因實現了Callable接口而重寫的有返回值的call()方法
    • (2.4)在call()方法調用完之後,就可以獲取到該線程的執行結果了
  • (3)Future的作用之一就是拿到線程的執行結果。

畫個圖的話,可以這樣表示:
在這裏插入圖片描述


介紹到這裏之後,有興趣的可以看一看爲什麼使用線程池,傳入一個Callable,返回的是一個Future。。。相信你肯定能看出個123來☺☺☺


end

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章