文章目錄
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