乾貨|Java Concurrent -- FutureTask 原始碼分析

中興開發者社群發表於2017-12-19

點選上方“中興開發者社群”,關注我們

每天讀一篇一線開發者原創好文

640?wx_fmt=png&wxfrom=5&wx_lazy=1

▍作者簡介

黃宇是從事java開發的開源軟體的愛好者。近些年致力於高併發、分散式大資料方向的研發工作。這篇文章主要講解了java concurrent包中future模式的原理和使用,相信大家能夠從中收到啟發。

在多執行緒執行時,對於需要有返回值的場景,常常使用Callable和Future的方式來進行,常見的一種使用方式如下:

640?wx_fmt=png&wxfrom=5&wx_lazy=1

執行上面的程式碼,在控制檯種等待三秒鐘之後列印出結果。程式碼非常簡單,但是有幾個問題需要弄清楚:

  1. 執行緒池是如何呼叫到Callable的call,結果是如何返回的。
  2. Future.get方法是如何阻塞住當前執行緒的

  3. 當Callable執行完是如何通知到阻塞在Future上面的執行緒的
  下面我們開始分析原始碼解答上述問題(原始碼基於jdk1.8)。

  首先從ExecutorService的submit(Callable )方法開始,此方法對應的實現類是ThreadPoolExecutor, 程式碼如下:

640?wx_fmt=png&wxfrom=5&wx_lazy=1

newTaskFor方法就是將callable轉換成了FutureTask,FutrueTask也繼承了RunnableFuture,獲取到RunnableFuture後,呼叫了execute(ftask) ,我們繼續向下跟程式碼

0?wx_fmt=png

0?wx_fmt=png

 圖片中的英文註釋已經說的比較清楚,我這裡在詳細說一下,第一行:

   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的程式碼較長,我們看關鍵的部分):

0?wx_fmt=png

  紅框部分就是觸發執行callbale執行的地方,圖片中第一行,firsttask就是我們上面建立的FutureTask,被Worker包裝了一下,在獲取Thread t,t的start方法就會觸發FutureTask的run方法。我們再進入FutureTask的run方法,程式碼如下:

0?wx_fmt=png

 具體呼叫callable的方法就在result = c.call(), 至此上面的問題1已經解答了,callable是在何時呼叫的,執行結果賦值給FutureTask的result變數。

   我們再看問題2,當呼叫FutureTask的get方法,如何阻塞呼叫執行緒的。在get方法中,如果判斷當前直接沒有結束,那麼就呼叫awaitDone方法,程式碼如下:

0?wx_fmt=png

 awaitDone程式碼如下:

0?wx_fmt=png

0?wx_fmt=png

方法有些複雜,從第一行看起,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()程式碼如下:

0?wx_fmt=png

finishCompletion程式碼如下

0?wx_fmt=png

 從第一行開始分析,迭代waiters,waiters是上面說到的AQS對應的雙向連結串列的頭結點,需要把整個waiters對應的連結串列的執行緒全部喚醒。在迴圈體中,呼叫了SupportLock的unpark方法。

   至此,FutureTask的大致過程已經分析完成,其中有些細節,筆者也沒有了解特別深入,希望讀者可以留言共同探討。

640?wx_fmt=jpeg

相關文章