簡談java併發FutureTask的實現

這篇文章主要介紹了簡談java併發FutureTask的實現,FutureTask都是用於獲取線程執行的返回結果。文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,,需要的朋友可以參考下

概述

在使用java多線程解決問題的時候,爲了提高效率,我們常常會異步處理一些計算任務並在最後異步的獲取計算結果,這個過程的實現離不開Future接口及其實現類FutureTask。FutureTask類實現了Runnable, Future接口,接下來我會通過源碼對該類的實現進行詳解。

使用

我們先看下FutureTask中的主要方法如下,可以看出FutureTask實現了任務及異步結果的集合功能。看到這塊的方法,大家肯定會有疑問,Runnable任務的run方法返回空,FutureTask如何依靠該方法獲取線程異步執行結果,這個問題,我們在下面給大家介紹。

//以下五個方法實現接口Future中方法
public boolean isCancelled(); 
public boolean isDone(); 
public boolean cancel();
public V get() throws InterruptedException, ExecutionException;
public V get(long timeout, TimeUnit unit);
//實現接口Runnable中方法
public void run();

我們在使用中會構造一個FutureTask對象,然後將FutureTask扔到另一個線程中執行,而主線程繼續執行其他業務邏輯,一段時間後主線程調用FutureTask的get方法獲取執行結果。下面我們看一個簡單的例子:

/**
* Created by yuanqiongqiong on 2019/4/9.
*/
public class FutureTaskTest {
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String []args) {
Callable callable = new AccCallable(1, 2);
FutureTask futureTask = new FutureTask(callable);
executorService.execute(futureTask);
System.out.println("go to do other things in main thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("go back in main thread");
try {
int result = (int) futureTask.get();
System.out.println("result is " + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
static class AccCallable implements Callable<Integer> {
private int a;
private int b;
public AccCallable(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public Integer call() throws Exception {
System.out.println("acc a and b in threadId = " + Thread.currentThread().getName());
return a + b;
}
}
}

輸出結果爲:

go to do other things in main thread
acc a and b in threadId = pool-1-thread-1
go back in main thread
result is 3

實現分析

在分析實現前,我們先想下如果讓我們實現一個類似FutureTask的功能,我們會如何做?因爲需要獲取執行結果,需要一個Object對象來存執行結果。任務執行時間不可控性,我們需要一個變量表示執行狀態。其他線程會調用get方法獲取結果,在沒達到超時的時候需要將線程阻塞或掛起。

因此需要一個隊列類似的結構存儲等待該結果的線程信息,這樣在任務執行線程完成後就可以喚醒這些阻塞或掛起的線程,得到結果。FutureTask的實際實現也是類似的邏輯,具體如下。

首先看下FutureTask的主要成員變量如下:

//futureTask執行狀態
private volatile int state;
//具體的執行任務,會在run方法中抵用callable.call()
private Callable<V> callable;
//執行結果
private Object outcome; 
//獲取結果的等待線程節點
private volatile WaitNode waiters;

對於執行狀態,在源碼中已經有了非常清晰的解釋,這裏我只是貼出源碼,不在進行說明,具體如下:

/**
* Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;

然後我們看下FutureTask的構造函數,如下:

public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
//構造函數傳入runnable對象時調用靜態工具類Executors的方法轉換爲一個callable對象
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}

如前所述,FutureTask的執行線程中會調用其run()方法執行任務,我們看下這塊邏輯:

public void run() {
//1.如果執行狀態不是NEW或者有其他線程執行該任務,直接返回
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
//2.如果執行狀態是NEW,即任務還沒執行,直接調用callable.call()方法獲取執行結果
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
//3.發生異常,更新status爲EXCEPTIONAL,喚醒掛起線程
setException(ex);
}
//4.如果結果成功返回,調用set方法將設置outcome,更改status執行狀態,喚醒掛起線程
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);
}
}

我們看下set函數的實現,具體看下4中的執行:

protected void set(V v) {
//將執行狀態變更爲COMPLETING
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
//設置執行結果
outcome = v;
//設置執行狀態爲NORMAL
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
//執行完成後處理操作,具體就是遍歷阻塞鏈表,刪除鏈表節點,並喚醒每個節點關聯的線程
finishCompletion();
}
}

以上就是任務執行線程做的邏輯,以上邏輯也回答了FutureTask如何得到執行結果的疑問。下面我們看下用戶調用get方法獲取執行結果時的實現邏輯,這個時候FutureTask可能處理各種狀態,即可能沒有執行,執行中,已完成,發生異常等,具體如下:

public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
//執行狀態是NEW或者COMPLETING時執行awaitDone將線程加入等待隊列中並掛起線程
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
//根據執行狀態status進行結果封裝
return report(s);
}
//我理解這塊是get的核心邏輯
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
//如果設置了超時時間,計算還有多長時間超時
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
//如果當前線程被中斷,刪除等待隊列中的節點,並拋出異常
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
//如果執行狀態已經完成或者發生異常,直接跳出自旋返回
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
//如果執行狀態是正在執行,說明線程已經被加入到等待隊列中,放棄cpu進入下次循環(真正的自旋)
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//第一次進入循環,創建節點
else if (q == null)
q = new WaitNode();
//將節點加入到等待隊列中,waiters相當於頭階段,不斷將頭結點更新爲新節點
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
//如果設置了超時時間,在進行下次循環前查看是否已經超時,如果超時刪除該節點進行返回
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
//掛起當前節點
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}

這裏需要說明一點,FutureTask中的阻塞隊列新加入的節點都在頭結點並且next指向之前的頭結點,waitars指針總是指向新加入節點,通過waitars可以遍歷整個等待隊列,具體截圖如下。此外等待隊列節點結構很簡單成員變量只有線程引用和next指針,這裏再列出器接口。

futureTask等待隊列

讀到這裏,相信大家已經對FutureTask的實現細節有了一定的認識。此外,FutureTask沒有使用鎖而是使用Unsafe的是CAS的原子操作來解決競爭問題,減少了鎖帶來的上下文切換的開銷,提高了效率。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。

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