通過FutureTask的源碼我們可以看到FuturenTask類實現了RunnableFuture介面,繼承了Runnable和Future介面。
public class FutureTask implements RunnableFuture
public interface RunnableFuture extends Runnable, Future
FutureTask可以交給Executor執行,也可以由調用線程直接執行(FutureTask.run())。根據FutureTask.run()方法被執行的時機,FutureTask主要有以下幾種狀態:
1、未啟動。FutureTask.run()方法還沒有被執行之前,FutureTask處於未啟動狀態。當建立一個FutureTask,且沒有執行FutureTask.run()方法之前,這個FutureTask處於未啟動狀態。
2、已啟動。FutureTask.run()方法被執行的過程中,FutureTask處於已啟動狀態。
3、已完成。FutureTask.run()方法執行完後正常結束,或被取消(FutureTask.cancel(…)),或執行FutureTask.run()方法時拋出異常而異常結束,FutureTask處於完成狀態。
下面我們來看看這幾種狀態的變換過程示意圖:
當FutureTask處於未啟動或已啟動狀態時,執行FutureTask.get()方法將導致調用線程阻塞;當FutureTask處於完成狀態時,執行FutureTask.get()方法將導致調用線程立即返回結果或拋出異常。
當FutureTask處於未啟動狀態時,執行FutureTask.cancel()方法將導致此任務永遠不會被執行;當FutureTask處於已啟動狀態時,執行FutureTask.cancel(true)方法將以中斷執行此任務線程的方式來試圖停止任務;當FutureTask處於已啟動狀態時,執行FutureTask.cancel(false)方法將不會對正在執行此任務的線程產生影響(讓正在執行的任務運行完成);當FutureTask處於完成狀態時,執行FutureTask.cancel(…)方法將返回false。
下面我們看下get和cancel執行過程
FutureTask使用
FutureTask可以通過Executor執行,也可以通過ExecutorService.submit返回一個FutureTask,然後執行FutureTask裡面的各種方法。
當一個線程需要等待另一個線程把某個任務執行完後它才能繼續執行,此時可以使用FutureTask。假設有多個線程執行若干任務,每個任務最多隻能被執行一次。當多個線程試圖同時執行同一個任務時,只允許一個線程執行任務,其他線程需要等待這個任務執行完後才能繼續執行
public class FutureTaskTest { private final ConcurrentMap<Object,Future<String>> taskCache = new ConcurrentHashMap<Object,Future<String>>(); public String executionTask(final String taskName) throws ExecutionException,InterruptedException{ for(;;){ Future<String> future = taskCache.get(taskName); if(future == null){ Callable<String> task = new Callable<String>(){ @Override public String call() throws Exception { return taskName; } }; FutureTask<String> futureTask = new FutureTask<String>(task); // 如果存在taskName則不往map裡放,傳回值是放入的futureTask future = taskCache.putIfAbsent(taskName, futureTask); if(future == null){ future = futureTask; futureTask.run(); } try { return futureTask.get(); } catch (Exception e) { taskCache.remove(taskName, future); e.printStackTrace(); } } } }}
把上面的代碼轉換成流程圖如下所示:
當兩個線程試圖同時執行同一個任務時,如果Thread 1執行putIfAbsent後Thread 2執行get擷取任務,那麼接下來Thread 2將在future.get()等待,直到Thread 1執行完futureTask.run()後Thread 2才能從(FutureTask.get())返回。 FutureTask實現原理
FutureTask的實現基於AbstractQueuedSynchronizer(AQS)。java.util.concurrent中的很多可阻塞類(比如ReentrantLock)都是基於AQS來實現的。AQS是一個同步架構,它提供通用機制來原子性管理同步狀態、阻塞和喚醒線程,以及維護被阻塞線程的隊列。JDK 6中AQS被廣泛使用,基於AQS實現的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask。
每一個基於AQS實現的同步器都會包含兩種類型的操作,第一個是至少一個acquire操作。這個操作阻塞調用線程,除非直到AQS的狀態允許這個線程繼續執行。FutureTask的acquire操作為get()/get(long timeout,TimeUnit unit)方法調用。
另一個是至少一個release操作。這個操作改變AQS的狀態,改變後的狀態可允許一個或多個阻塞線程被解除阻塞。FutureTask的release操作包括run()方法和cancel(…)方法。基於“複合優先於繼承”的原則,FutureTask聲明了一個內部私人的繼承於AQS的子類Sync,對FutureTask所有公有方法的調用都會委託給這個內部子類。AQS被作為“模板方法模式”的基礎類提供給FutureTask的內部子類Sync,這個內部子類只需要實現狀態檢查和狀態更新的方法即可,這些方法將控制FutureTask的擷取和釋放操作。具體來說,Sync實現了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通過這兩個方法來檢查和更新同步狀態。
Sync是FutureTask的內部私人類,它繼承自AQS。建立FutureTask時會建立內部私人的成員對象Sync,FutureTask所有的的公有方法都直接委託給了內部私人的Sync。FutureTask.get()方法會調用AQS.acquireSharedInterruptibly(int arg)方法,這個方法的執行過程如下。
1、調用AQS.acquireSharedInterruptibly(int arg)方法,這個方法首先會回調在子類Sync中實現的tryAcquireShared()方法來判斷acquire操作是否可以成功。acquire操作可以成功的條件為:state為執行完成狀態RAN或已取消狀態CANCELLED,且runner不為null。
2、如果成功則get()方法立即返回。如果失敗則到線程等待隊列中去等待其他線程執行release操作。
3、當其他線程執行release操作(比如FutureTask.run()或FutureTask.cancel(…))喚醒當前線程後,當前線程再次執行tryAcquireShared()將返回正值1,當前線程將離開線程等待隊列並喚醒它的後繼線程(這裡會產生級聯喚醒的效果,後面會介紹)。
4、最後返回計算的結果或拋出異常。
下面我們看看FutureTask.run()的執行過程
1、執行在建構函式中指定的任務(Callable.call())。
2、以原子方式來更新同步狀態(調用AQS.compareAndSetState(int expect,int update),設定state為執行完成狀態RAN)。如果這個原子操作成功,就設定代表計算結果的變數result的值為Callable.call()的傳回值,然後調用AQS.releaseShared(int arg)。
3、AQS.releaseShared(int arg)首先會回調在子類Sync中實現的tryReleaseShared(arg)來執行release操作(設定運行任務的線程runner為null,然會返回true);AQS.releaseShared(int arg),然後喚醒線程等待隊列中的第一個線程。
4、調用FutureTask.done()。
當執行FutureTask.get()方法時,如果FutureTask不是處於執行完成狀態RAN或已取消狀態CANCELLED,當前執行線程將到AQS的線程等待隊列中等待(見下圖的線程A、B、C和D)。當某個線程執行FutureTask.run()方法或FutureTask.cancel(…)方法時,會喚醒線程等待隊列的第一
個線程(見下圖所示的線程E喚醒線程A)。
假設開始時FutureTask處於未啟動狀態或已啟動狀態,等待隊列中已經有3個線程(A、B和C)在等待。此時,線程D執行get()方法將導致線程D也到等待隊列中去等待。當線程E執行run()方法時,會喚醒隊列中的第一個線程A。線程A被喚醒後,首先把自己從隊列中刪除,然後喚醒它的後繼線程B,最後線程A從get()方法返回。線程B、C和D重複A線程的處理流程。最終,在隊列中等待的所有線程都被級聯喚醒並從get()方法返回。 多線程正式環境問題定位
多線程的問題往往不好定位,主要大腦類比各種可能出現問題的情境,然後通過分析日誌、系統狀態和dump線程,下面我們介紹幾種方式方便我們定位問題。 在Linux命令列下使用TOP命令查看每個進程的情況
我們的程式是Java應用,所以只需要關注COMMAND是Java的效能資料,COMMAND表示啟動當前進程的命令,在Java進程這一行裡有時候可以看到CPU利用率是>100%,不用擔心,這個是當前機器所有核加在一起的CPU利用率 使用top的互動命令數字1查看每個CPU的效能資料
命令列顯示了CPU3,說明這是一個4核的虛擬機器,平均每個CPU利用率在3%以下。如果這裡顯示CPU利用率100%,則很有可能程式裡寫了一個死迴圈
參數說明
us:使用者空間佔用CPU百分比
1.0% sy:核心空間佔用CPU百分比
0.0% ni:使用者進程空間改變過優先順序的進程佔用CPU百分比
98.7% id:空閑CPU百分比
0.0% wa:等待輸入/輸出CPU時間百分比 使用top的互動命令H查看每個線程的效能資訊
這裡我們需要特別主要三種情況:
1、某個線程CPU利用率一直100%,則說明是這個線程有可能有死迴圈,我們可以記下這個PID。
2、某個線程一直在TOP 10的位置,這說明這個線程可能有效能問題。
3、CPU利用率高的幾個線程在不停變化,說明並不是由某一個線程導致CPU偏高。
如果是第一種情況,也有可能是GC造成,可以用jstat命令看一下GC情況,看看是不是因為持久代或年老代滿了,產生Full GC,導致CPU利用率持續飆高
還可以把線程dump下來,看看究竟是哪個線程、執行什麼代碼造成的CPU利用率高。執行以下命令,把線程dump到檔案dump.test.log裡。執行如下命令。
sudo -u admin /opt/usr/java/bin/jstack 31177 > /home/fuyuwei/dump.test.log"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in Object.wait() [0x0000000052423000]java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on (a org.apache.tomcat.util.net.AprEndpoint$Worker)at java.lang.Object.wait(Object.java:485)at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)- locked (a org.apache.tomcat.util.net.AprEndpoint$Worker)at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)at java.lang.Thread.run(Thread.java:662)
dump出來的線程ID(nid)是十六進位的,而我們用TOP命令看到的線程ID是十進位的,所以要用printf命令轉換一下進位。然後用十六進位的ID去dump裡找到對應的線程
例如我們用top定位到PID=1234,然後轉成十六進位
printf "%x\n" 12344d2
這裡我們就簡單介紹這幾種方式吧。