乾貨|Java Concurrent -- FutureTask 原始碼分析
點選上方“中興開發者社群”,關注我們
每天讀一篇一線開發者原創好文
▍作者簡介
黃宇是從事java開發的開源軟體的愛好者。近些年致力於高併發、分散式大資料方向的研發工作。這篇文章主要講解了java concurrent包中future模式的原理和使用,相信大家能夠從中收到啟發。
在多執行緒執行時,對於需要有返回值的場景,常常使用Callable和Future的方式來進行,常見的一種使用方式如下:
執行上面的程式碼,在控制檯種等待三秒鐘之後列印出結果。程式碼非常簡單,但是有幾個問題需要弄清楚:
1. 執行緒池是如何呼叫到Callable的call,結果是如何返回的。
2. Future.get方法是如何阻塞住當前執行緒的
3. 當Callable執行完是如何通知到阻塞在Future上面的執行緒的
下面我們開始分析原始碼解答上述問題(原始碼基於jdk1.8)。
首先從ExecutorService的submit(Callable )方法開始,此方法對應的實現類是ThreadPoolExecutor, 程式碼如下:
newTaskFor方法就是將callable轉換成了FutureTask,FutrueTask也繼承了RunnableFuture,獲取到RunnableFuture後,呼叫了execute(ftask) ,我們繼續向下跟程式碼
圖片中的英文註釋已經說的比較清楚,我這裡在詳細說一下,第一行:
int c = ctl.get(), 其中ctl是一個AtomicInteger,每當向執行緒池放入一個callable或者runnable,這個clt就+1,int c 獲取到的就是當前執行緒池目前的任務數量,
情況1:如果c 小於當前執行緒池維護最小的工作執行緒(CorePoolSize)時,那麼執行緒池就嘗試開啟一個工作執行緒來執行傳入的callable,也就是下面執行的addWorker(command,true), 此方法會自動檢查runState和workerCount,從而防止將新增的錯誤。
情況2:如果c大於等於CorePoolSize時,要判斷當前執行緒池是否正在執行,其次判斷callable是否可以加入佇列(有可能加入不成功,例如使用了有界佇列,佇列可能滿了),如果以上條件都滿足,那麼再次通過ctl獲取一次數量,做二次檢查,按照官方註釋,寫的是有可能在做完第一次檢查後,當前執行緒池掛了,或者其他錯誤,那麼此時需要做回滾,也就是if中的remove(command),從佇列中刪除剛才放入的callable,然後在呼叫reject拒絕此任務,如果二次檢查也沒有問題,再呼叫addWorker(null,false),我們發現引數和情況1不同,任務為null,意思是不要為當前任務分配工作執行緒(因為任務已經加入佇列了,不需要立刻執行),false表示判斷當前執行緒池工作執行緒的數量是否超過了maxPoolsize,有興趣的同學,可以仔細研究下addWorker方法的原始碼。
情況3:如果呼叫addWorker返回false,說明當前執行緒池的工作執行緒數量超過了maxpoolSize了,那麼新的任務需要拒絕(ps:上面描述的內容需要讀者對執行緒池中,任務、工作執行緒、corePoolsize、maxPoolsize以及任務佇列都熟悉才能理解)
根據上述分析,觸發callable執行的程式碼在addWorker中,程式碼如下(addWorker的程式碼較長,我們看關鍵的部分):
紅框部分就是觸發執行callbale執行的地方,圖片中第一行,firsttask就是我們上面建立的FutureTask,被Worker包裝了一下,在獲取Thread t,t的start方法就會觸發FutureTask的run方法。我們再進入FutureTask的run方法,程式碼如下:
具體呼叫callable的方法就在result = c.call(), 至此上面的問題1已經解答了,callable是在何時呼叫的,執行結果賦值給FutureTask的result變數。
我們再看問題2,當呼叫FutureTask的get方法,如何阻塞呼叫執行緒的。在get方法中,如果判斷當前直接沒有結束,那麼就呼叫awaitDone方法,程式碼如下:
awaitDone程式碼如下:
方法有些複雜,從第一行看起,deadline 經過計算 = 0,因為timed為false,向下看,之所以有for(;;)無線迴圈,因為下面用到了cas,需要自旋執行最終成功。進入迴圈體,首先判斷當前呼叫get方法的執行緒是否被interrupt,如果被interrupt,那麼直接返回不會阻塞,後面的幾個if基本都是判斷狀態,如果當前FutureTask已經結束,那麼就直接返回,不阻塞,然後初始化了WaitNode,當程式碼執行到紅色框部分,說明當前futureTask沒有執行完成,那麼需要把上面初始化的WaitNode加入到佇列,所謂的佇列就是AQS(AbstractQueuedSynchronizer), AQS底層維護了一個雙向連結串列,當多執行緒呼叫FutureTask的get方法時,多個執行緒就會被加入到這個連結串列,但是加入佇列的過程需要鎖的控制,這裡的控制就是CAS,多個執行緒同時呼叫get方法時,當執行到這個紅色框部分,每次只有一個執行緒可以加入連結串列成功,由於外層由for(;;),所以失敗的執行緒會再次執行,然後又有一個執行緒執行成功,以此類推,所有的執行緒都會執行成功,加入佇列。在向下看,由於我們呼叫的get方法,是沒有時間限制的,所有timed=false,所以不會進入綠色框的程式碼塊,對於加入佇列成功的執行緒,只是當前執行緒物件加入佇列,但是執行緒還在執行,此時queued=true,那麼下次迴圈就會進入到藍色框部分,LockSupport.park(),這個方法是阻塞,這個方法和wait()/notify()/notifyAll()很類似,但是wait之後,如果想喚醒指定的執行緒無法實現,只能notifyAll,不是很智慧,LockSupport.park(thread)方法需要傳入一個Thread,也就是阻塞的執行緒,在呼叫LockSupport.unPark(thread),傳入的thread只要和park的thread相同,就可以喚醒指定的執行緒。
通過上面的描述,我們知道了,在get方法中,主要是通過park方法進行阻塞的,那是再哪裡喚醒阻塞的執行緒的呢?其實上面也有提過,是再run方法中,因為run方法是呼叫callable.call的地方,callable執行成功後,會把值付給result變數,然後再呼叫set方法,在set方法中會呼叫finishCompletion,然後此方法再呼叫LockSupport.unpark()程式碼如下:
finishCompletion程式碼如下
從第一行開始分析,迭代waiters,waiters是上面說到的AQS對應的雙向連結串列的頭結點,需要把整個waiters對應的連結串列的執行緒全部喚醒。在迴圈體中,呼叫了SupportLock的unpark方法。
至此,FutureTask的大致過程已經分析完成,其中有些細節,筆者也沒有了解特別深入,希望讀者可以留言共同探討。
相關文章
- Java集合乾貨——CopyOnWriteArrayList原始碼分析Java原始碼
- Java集合乾貨——ArrayList原始碼分析Java原始碼
- Java集合乾貨1——ArrayList原始碼分析Java原始碼
- FutureTask原始碼分析筆記原始碼筆記
- Java非同步程式設計——深入原始碼分析FutureTaskJava非同步程式設計原始碼
- 併發程式設計—— FutureTask 原始碼分析程式設計原始碼
- 技術乾貨 | WebRTC ADM 原始碼流程分析Web原始碼
- Java多執行緒類FutureTask原始碼閱讀以及淺析Java執行緒原始碼
- 原始碼|使用FutureTask的正確姿勢原始碼
- FutureTask原始碼解析(1)——預備知識原始碼
- Android FutureTask 分析Android
- 併發系列(二)——FutureTask類原始碼簡析原始碼
- java 原始碼分析 —BooleanJava原始碼Boolean
- Java 原始碼如何分析?Java原始碼
- Java:HashMap原始碼分析JavaHashMap原始碼
- Java Collections 原始碼分析Java原始碼
- 【java乾貨】java怎麼寫APPJavaAPP
- Java容器原始碼學習--ArrayList原始碼分析Java原始碼
- Java String原始碼分析Java原始碼
- 【Java】ServiceLoader原始碼分析Java原始碼
- 【Java集合】ArrayList原始碼分析Java原始碼
- PowerUsageSummary.java原始碼分析Java原始碼
- BatteryStatsHelper.java原始碼分析BATJava原始碼
- JAVA集合:ArrayList原始碼分析Java原始碼
- Java 集合包原始碼分析Java原始碼
- Java LinkedList 原始碼分析Java原始碼
- AI客服上線 乾貨 乾貨 全是乾貨!AI
- Java原始碼分析:Guava之不可變集合ImmutableMap的原始碼分析Java原始碼Guava
- 乾貨:ANR日誌分析全面解析
- jdk原始碼閱讀(主要:util,lang,concurrent)(一)JDK原始碼
- Java集合原始碼分析(十四):TreeMapJava原始碼
- java基礎:ArrayList — 原始碼分析Java原始碼
- java基礎:HashMap — 原始碼分析JavaHashMap原始碼
- java基礎:Enum — 原始碼分析Java原始碼
- java基礎:Integer — 原始碼分析Java原始碼
- java基礎:TreeMap — 原始碼分析Java原始碼
- Java集合原始碼分析(九)——HashSetJava原始碼
- java集合原始碼分析(六):HashMapJava原始碼HashMap