前言
FutureTask 是一個同步工具類,它實現了Future語義,表示了一種抽象的可生成結果的計算。在包括執行緒池在內的許多工具類中都會用到,弄懂它的實現將有利於我們更加深入地理解Java非同步操作實現。
在分析它的原始碼之前, 我們需要先了解一些預備知識。本篇我們先來看看FutureTask 中所使用到的介面:Runnable
、Callable
、Future
、RunnableFuture
以及所使用到的工具類Executors
,Unsafe
。
FutureTask所使用到的介面
Runnable介面
在前面Thread類原始碼解讀的系列文章中我們說過, 建立執行緒最重要的是傳遞一個run()
方法, 這個run方法定義了這個執行緒要做什麼事情, 它被抽象成了Runnable
介面:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
但是, 可以發現, 這個方法並沒有任何返回值.
如果我們希望執行某種型別的操作並拿到它的執行結果, 該怎麼辦呢?
從 Runnable 到 Callable
要從某種型別的操作中拿到執行結果, 最簡單的方式自然是令這個操作自己返回操作結果, 則相較於run
方法返回void
,我們可以令一個操作返回特定型別的物件, 這種思路的實現就是Callable
介面:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
對比Callable介面與Runnable介面, 我們可以發現它們最大的不同點在於:
- Callable有返回值
- Callable可以丟擲異常
關於有返回值這點,我們並不意外,因為這就是我們的需求,call方法的返回值型別採用的泛型,該型別是我們在建立Callable物件的時候指定的。
除了有返回值外,相較於Runnable介面,Callable還可以丟擲異常,這點看上去好像沒啥特別的,但是卻有大用處——這意味著如果在任務執行過程中發生了異常,我們可以將它向上丟擲給任務的呼叫者來妥善處理,我們甚至可以利用這個特性來中斷一個任務的執行。而Runnable介面的run方法不能丟擲異常,只能在方法內部catch住處理,喪失了一定的靈活性。
使用Callable介面解決了返回執行結果的問題, 但是也帶來了一個新的問題:
如何獲得執行結果?
有的同學可能就要說了, 這還不簡單? 直接拿不就好了, 看我的:
public static void main(String[] args) {
Callable<String> myCallable = () -> "This is the results.";
try {
String result = myCallable.call();
System.out.println("Callable 執行的結果是: " + result);
} catch (Exception e) {
System.out.println("There is a exception.");
}
}
這種方法確實可以, 但是它存在幾個問題:
- call方法是在當前執行緒中直接呼叫的, 無法利用多執行緒。
- call方法可能是一個特別耗時的操作, 這將導致程式停在
myCallable.call()
呼叫處, 無法繼續執行, 直到call方法返回。 - 如果call方法始終不返回, 我們沒辦法中斷它的執行。
因此, 理想的操作應當是, 我們將call方法提交給另外一個執行緒執行, 並在合適的時候, 判斷任務是否完成, 然後獲取執行緒的執行結果或者撤銷任務, 這種思路的實現就是Future介面:
Future介面
Future介面被設計用來代表一個非同步操作的執行結果。你可以用它來獲取一個操作的執行結果、取消一個操作、判斷一個操作是否已經完成或者是否被取消
public interface Future<V> {
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
}
Future介面一共定義了5個方法:
-
get()
- 該方法用來獲取執行結果, 如果任務還在執行中, 就阻塞等待;
-
get(long timeout, TimeUnit unit)
- 該方法同get方法類似, 所不同的是, 它最多等待指定的時間, 如果指定時間內任務沒有完成, 則會丟擲
TimeoutException
異常;
- 該方法同get方法類似, 所不同的是, 它最多等待指定的時間, 如果指定時間內任務沒有完成, 則會丟擲
-
cancel(boolean mayInterruptIfRunning)
- 該方法用來嘗試取消一個任務的執行, 它的返回值是boolean型別, 表示取消操作是否成功.
-
isCancelled()
- 該方法用於判斷任務是否被取消了。如果一個任務在正常執行完成之前被cancel掉了, 則返回true
-
isDone()
-
如果一個任務已經結束, 則返回true。注意, 這裡的任務結束包含了以下三種情況:
- 任務正常執行完畢
- 任務丟擲了異常
- 任務已經被取消
-
關於cancel方法,這裡要補充說幾點:
首先有以下三種情況之一的,cancel操作一定是失敗的:
- 任務已經執行完成了
- 任務已經被取消過了
- 任務因為某種原因不能被取消
其它情況下,cancel操作將返回true。值得注意的是,cancel操作返回true並不代表任務真的就是被取消了,這取決於發動cancel狀態時任務所處的狀態:
- 如果發起cancel時任務還沒有開始執行,則隨後任務就不會被執行;
-
如果發起cancel時任務已經在執行了,則這時就需要看
mayInterruptIfRunning
引數了:- 如果
mayInterruptIfRunning
為true, 則當前在執行的任務會被中斷 - 如果
mayInterruptIfRunning
為false, 則可以允許正在執行的任務繼續執行,直到它執行完
- 如果
這個cancel方法的規範看起來有點繞,現在不太理解沒關係,後面結合例項去看就容易弄明白了,我們將在下一篇分析FutureTask原始碼的時候詳細說說FutureTask對這一方法的實現。
RunnableFuture 介面
RunnableFuture介面人如其名, 就是同時實現了Runnable介面和Future介面:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
我們下一篇開始分析FutureTask的原始碼的時候就將看到,FutureTask實現了該介面,也就是相當於它同時實現了Runnable介面和Future介面。
有的同學可能會對這個介面產生疑惑,既然已經繼承了Runnable,該介面自然就繼承了run方法,為什麼要在該介面的內部再寫一個run方法?
單純從理論上來說,這裡確實是沒有必要的,再多寫一遍,我覺得大概就是為了看上去直觀一點,便於文件或者UML圖展示。
FutureTask所使用到的工具類
Executors
Executors 是一個用於建立執行緒池的工廠類,關於執行緒池的概念,我們以後再說。這個類同時也提供了一些有用的靜態方法。
前面我們提到了Callable介面,它是JDK1.5才引入的,而Runnable介面在JDK1.0就有了,我們有時候需要將一個已經存在Runnable物件轉換成Callable物件,Executors工具類為我們提供了這一實現:
public class Executors {
/**
* Returns a {@link Callable} object that, when
* called, runs the given task and returns the given result. This
* can be useful when applying methods requiring a
* {@code Callable} to an otherwise resultless action.
* @param task the task to run
* @param result the result to return
* @param <T> the type of the result
* @return a callable object
* @throws NullPointerException if task null
*/
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
/**
* A callable that runs given task and returns given result
*/
static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}
}
可以明顯看出來,這個方法採用了設計模式中的介面卡模式,將一個Runnable型別物件適配成Callable型別。
因為Runnable介面沒有返回值, 所以為了與Callable相容, 我們額外傳入了一個result引數, 使得返回的Callable物件的call方法直接執行Runnable的run方法, 然後返回傳入的result引數。
有的同學要說了, 你把result引數傳進去, 又原封不動的返回出來, 有什麼意義呀?
這樣做確實沒什麼意義, result引數的存在只是為了將一個Runnable型別適配成Callable型別.
Unsafe
Unsafe類對於併發程式設計來說是個很重要的類,如果你稍微看過J.U.C裡的原始碼(例如我們前面講AQS系列的文章裡),你會發現到處充斥著這個類的方法呼叫。
這個類的最大的特點在於,它提供了硬體級別的CAS原子操作。
可能有的同學會覺得這並沒有什麼了不起,CAS的概念都被說爛了。但是,CAS可以說是實現了最輕量級的鎖,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中的一個執行緒能成功地更新變數的值,而其他的執行緒將失敗。然而,失敗的執行緒並不會被掛起。
CAS操作包含了三個運算元: 需要讀寫的記憶體位置,進行比較的原值,擬寫入的新值。
在Unsafe類中,實現CAS操作的方法是: compareAndSwapXXX
例如:
public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);
-
obj
是我們要操作的目標物件 -
offset
表示了目標物件中,對應的屬性的記憶體偏移量 -
expect
是進行比較的原值 -
update
是擬寫入的新值。
所以該方法實現了對目標物件obj中的某個成員變數(field
)進行CAS操作的功能。
那麼,要怎麼獲得目標field
的記憶體偏移量offset
呢? Unsafe類為我們提供了一個方法:
public native long objectFieldOffset(Field field);
該方法的引數是我們要進行CAS操作的field物件,要怎麼獲得這個field物件呢?最直接的辦法就是通過反射了:
Class<?> k = FutureTask.class;
Field stateField = k.getDeclaredField("state");
這樣一波下來,我們就能對FutureTask的state屬性進行CAS操作了o( ̄▽ ̄)o
除了compareAndSwapObject
,Unsafe類還提供了更為具體的對int和long型別的CAS操作:
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
public native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
從方法簽名可以看出,這裡只是把目標field的型別限定成int和long型別,而不是通用的Object.
最後,FutureTask還用到了一個方法:
public native void putOrderedInt(Object obj, long offset, int value);
可以看出,該方法只有三個引數,所以它沒有比較再交換的概念,某種程度上就是一個賦值操作,即設定obj物件中offset偏移地址對應的int型別的field的值為指定值。這其實是Unsafe的另一個方法putIntVolatile
的有序或者有延遲的版本,並且不保證值的改變被其他執行緒立即看到,只有在field被volatile
修飾並且期望被意外修改的時候使用才有用。
那麼putIntVolatile
方法的定義是什麼呢?
public native void putIntVolatile(Object obj, long offset, int value);
該方法設定obj物件中offset偏移地址對應的整型field的值為指定值,支援volatile store語義。由此可以看出,當操作的int型別field本身已經被volatile修飾時,putOrderedInt
和putIntVolatile
是等價的。
好了,到這裡,基本需要用到的預備知識我們都學習完了,障礙已經掃清,下一篇我們就可以愉快地看FutureTask
的原始碼了(๑¯∀¯๑)
(完)
檢視更多系列文章: 系列文章目錄