摘要:最后,我們會通過對源代碼的剖析深入了解線程池的運行過程和具體設計,真正達到知其然而知其所以然的水平。創建線程池既然線程池是一個類,那么最直接的使用方法一定是一個類的對象,例如。單線程線程池單線程線程
我們一般不會選擇直接使用線程類Thread進行多線程編程,而是使用更方便的線程池來進行任務的調度和管理。線程池就像共享單車,我們只要在我們有需要的時候去獲取就可以了。甚至可以說線程池更棒,我們只需要把任務提交給它,它就會在合適的時候運行了。但是如果直接使用Thread類,我們就需要在每次執行任務時自己創建、運行、等待線程了,而且很難對線程進行整體的管理,這可不是一件輕松的事情。既然我們已經有了線程池,那還是把這些麻煩事交給線程池來處理吧。
這篇文章將會從線程池的概念與一般使用入手,首先讓大家可以了解線程池的基本使用方法,之后會介紹實踐中最常用的四種線程池。最后,我們會通過對JDK源代碼的剖析深入了解線程池的運行過程和具體設計,真正達到知其然而知其所以然的水平。雖然只要了解了API就可以滿足一般的日常使用了,但是只有當我們真正厘清了多線程相關的知識點,才能在面對多線程的實踐與面試問題時做到游刃有余、成竹在胸。
本文是一系列多線程文章中的第三篇,主要講解了線程池相關的知識,這個系列總共有十篇文章,前五篇暫定結構如下,感興趣的讀者可以關注一下:
并發基本概念——當我們在說“并發、多線程”,說的是什么?
多線程入門——這一次,讓我們完全掌握Java多線程(2/10)
線程池使用與原理剖析——本文
線程同步機制
并發常見問題
線程池的使用方法一般我們最常用的線程池實現類是ThreadPoolExecutor,我們接下來會介紹這個類的基本使用方法。JDK已經對線程池做了比較好的封裝,相信這個過程會非常輕松。
創建線程池既然線程池是一個Java類,那么最直接的使用方法一定是new一個ThreadPoolExecutor類的對象,例如ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue
下面就是這個構造器的方法簽名:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueworkQueue)
各個參數分別表示下面的含義:
corePoolSize,核心線程池大小,一般線程池會至少保持這么多的線程數量;
maximumPoolSize,最大線程池大小,也就是線程池最大的線程數量;
keepAliveTime和unit共同組成了一個超時間,keepAliveTime是時間數量,unit是時間單位,單位加數量組成了最終的超時時間。這個超時時間表示如果線程池中包含了超過corePoolSize數量的線程,則在有線程空閑的時間超過了超時時間時該線程就會被銷毀;
workQueue是任務的阻塞隊列,在沒有線程池中沒有足夠的線程可用的情況下會將任務先放入到這個阻塞隊列中等待執行。這里傳入的隊列類型就決定了線程池在處理這些任務時的策略。
線程池中的阻塞隊列專門用于存放待執行的任務,在ThreadPoolExecutor中一個任務可以通過兩種方式被執行:第一種是直接在創建一個新的Worker時被作為第一個任務傳入,由這個新創建的線程來執行;第二種就是把任務放入一個阻塞隊列,等待線程池中的工作線程撈取任務進行執行。
上面提到的阻塞隊列是這樣的一種數據結構,它是一個隊列(類似于一個List),可以存放0到N個元素。我們可以對這個隊列進行插入和彈出元素的操作,彈出操作可以理解為是一個獲取并從隊列中刪除一個元素的操作。當隊列中沒有元素時,對這個隊列的獲取操作將會被阻塞,直到有元素被插入時才會被喚醒;當隊列已滿時,對這個隊列的插入操作將會被阻塞,直到有元素被彈出后才會被喚醒。這樣的一種數據結構非常適合于線程池的場景,當一個工作線程沒有任務可處理時就會進入阻塞狀態,直到有新任務提交后才被喚醒。
提交任務當創建了一個線程池之后我們就可以將任務提交到線程池中執行了。提交任務到線程池中相當簡單,我們只要把原來傳入Thread類構造器的Runnable對象傳入線程池的execute方法或者submit方法就可以了。execute方法和submit方法基本沒有區別,兩者的區別只是submit方法會返回一個Future對象,用于檢查異步任務的執行情況和獲取執行結果(異步任務完成后)。
我們可以先試試如何使用比較簡單的execute方法,代碼例子如下:
public class ThreadPoolTest { private static int count = 0; public static void main(String[] args) throws Exception { Runnable task = new Runnable() { public void run() { for (int i = 0; i < 1000000; ++i) { synchronized (ThreadPoolTest.class) { count += 1; } } } }; // 重要:創建線程池 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue關閉線程池()); // 重要:向線程池提交兩個任務 threadPool.execute(task); threadPool.execute(task); // 等待線程池中的所有任務完成 threadPool.shutdown(); while (!threadPool.awaitTermination(1L, TimeUnit.MINUTES)) { System.out.println("Not yet. Still waiting for termination"); } System.out.println("count = " + count); } }
上面的代碼中為了等待線程池中的所有任務執行完已經使用了shutdown()方法,關閉線程池的方法主要有兩個:
shutdown(),有序關閉線程池,調用后線程池會讓已經提交的任務完成執行,但是不會再接受新任務。
shutdownNow(),直接關閉線程池,線程池中正在運行的任務會被中斷,正在等待執行的任務不會再被執行,但是這些還在阻塞隊列中等待的任務會被作為返回值返回。
監控線程池運行狀態我們可以通過調用線程池對象上的一些方法來獲取線程池當前的運行信息,常用的方法有:
getTaskCount,線程池中已完成、執行中、等待執行的任務總數估計值。因為在統計過程中任務會發生動態變化,所以最后的結果并不是一個準確值;
getCompletedTaskCount,線程池中已完成的任務總數,這同樣是一個估計值;
getLargestPoolSize,線程池曾經創建過的最大線程數量。通過這個數據可以知道線程池是否充滿過,也就是達到過maximumPoolSize;
getPoolSize,線程池當前的線程數量;
getActiveCount,當前線程池中正在執行任務的線程數量估計值。
四種常用線程池很多情況下我們也不會直接創建ThreadPoolExecutor類的對象,而是根據需要通過Executors的幾個靜態方法來創建特定用途的線程池。目前常用的線程池有四種:
可緩存線程池,使用Executors.newCachedThreadPool方法創建
定長線程池,使用Executors.newFixedThreadPool方法創建
延時任務線程池,使用Executors.newScheduledThreadPool方法創建
單線程線程池,使用Executors.newSingleThreadExecutor方法創建
下面通過這些靜態方法的源碼來具體了解一下不同類型線程池的特性與適用場景。
可緩存線程池JDK中的源碼我們通過在IDE中進行跳轉可以很方便地進行查看,下面就是Executors.newCachedThreadPool方法中的源代碼。從代碼中我們可以看到,可緩存線程池其實也是通過直接創建ThreadPoolExecutor類的構造器創建的,只是其中的參數都已經被設置好了,我們可以不用做具體的設置。所以我們要觀察的重點就是在這個方法中具體產生了一個怎樣配置的ThreadPoolExecutor對象,以及這樣的線程池適用于怎樣的場景。
從下面的代碼中,我們可以看到,傳入ThreadPoolExecutor構造器的值有:
- corePoolSize核心線程數為0,代表線程池中的線程數可以為0 - maximumPoolSize最大線程數為Integer.MAX_VALUE,代表線程池中最多可以有無限多個線程 - 超時時間設置為60秒,表示線程池中的線程在空閑60秒后會被回收 - 最后傳入的是一個`SynchronousQueue`類型的阻塞隊列,代表每一個新添加的任務都要馬上有一個工作線程進行處理
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }
所以可緩存線程池在添加任務時會優先使用空閑的線程,如果沒有就創建一個新線程,線程數沒有上限,所以每一個任務都會馬上被分配到一個工作線程進行執行,不需要在阻塞隊列中等待;如果線程池長期閑置,那么其中的所有線程都會被銷毀,節約系統資源。
優點
任務在添加后可以馬上執行,不需要進入阻塞隊列等待
在閑置時不會保留線程,可以節約系統資源
缺點
對線程數沒有限制,可能會過量消耗系統資源
適用場景
適用于大量短耗時任務和對響應時間要求較高的場景
定長線程池傳入ThreadPoolExecutor構造器的值有:
corePoolSize核心線程數和maximumPoolSize最大線程數都為固定值nThreads,即線程池中的線程數量會保持在nThreads,所以被稱為“定長線程池”
超時時間被設置為0毫秒,因為線程池中只有核心線程,所以不需要考慮超時釋放
最后一個參數使用了無界隊列,所以在所有線程都在處理任務的情況下,可以無限添加任務到阻塞隊列中等待執行
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }
定長線程池中的線程數會逐步增長到nThreads個,并且在之后空閑線程不會被釋放,線程數會一直保持在nThreads個。如果添加任務時所有線程都處于忙碌狀態,那么就會把任務添加到阻塞隊列中等待執行,阻塞隊列中任務的總數沒有上限。
優點
線程數固定,對系統資源的消耗可控
缺點
在任務量暴增的情況下線程池不會彈性增長,會導致任務完成時間延遲
使用了無界隊列,在線程數設置過小的情況下可能會導致過多的任務積壓,引起任務完成時間過晚和資源被過度消耗的問題
適用場景
任務量峰值不會過高,且任務對響應時間要求不高的場景
延時任務線程池與之前的兩個方法不同,Executors.newScheduledThreadPool返回的是ScheduledExecutorService接口對象,可以提供延時執行、定時執行等功能。在線程池配置上有如下特點:
maximumPoolSize最大線程數為無限,在任務量較大時可以創建大量新線程執行任務
超時時間為0,線程空閑后會被立即銷毀
使用了延時工作隊列,延時工作隊列中的元素都有對應的過期時間,只有過期的元素才會被彈出
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
延時任務線程池實現了ScheduledExecutorService接口,主要用于需要延時執行和定時執行的情況。
單線程線程池單線程線程池中只有一個工作線程,可以保證添加的任務都以指定順序執行(先進先出、后進先出、優先級)。但是如果線程池里只有一個線程,為什么我們還要用線程池而不直接用Thread呢?這種情況下主要有兩種優點:一是我們可以通過共享的線程池很方便地提交任務進行異步執行,而不用自己管理線程的生命周期;二是我們可以使用任務隊列并指定任務的執行順序,很容易做到任務管理的功能。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue線程池的內部實現())); }
通過前面的內容我們其實已經可以在代碼中使用線程池了,但是我們為什么還要去深究線程池的內部實現呢?首先,可能有一個很功利性的目的就是為了面試,在面試時如果能準確地說出一些底層的運行機制與原理那一定可以成為過程中一個重要的亮點。
但是我認為學習探究線程池的內部實現的作用絕對不僅是如此,只有深入了解并厘清了線程池的具體實現,我們才能解決實踐中需要考慮的各種邊界條件。因為多線程編程所代表的并發編程并不是一個固定的知識點,而是實踐中不斷在發展和完善的一個知識門類。我們也許會需要同時考慮多個維度,最后得到一個特定于應用場景的解決方案,這就要求我們具備從細節著手構建出解決方案并做好各個考慮維度之間的取舍的能力。
而且我相信只要在某一個點上能突破到相當的深度,那么以后從這個點上向外擴展就會容易得多。也許在剛開始我們的探究會碰到非常大的阻力,但是我們要相信,最后我們可以得到的將不止是一個知識點而是一整個知識面。
查看JDK源碼的方式在IDE中,例如IDEA里,我們可以點擊我們樣例代碼里的ThreadPoolExecutor類跳轉到JDK中ThreadPoolExecutor類的源代碼。在源代碼中我們可以看到很多java.util.concurrent包的締造者大牛“Doug Lea”所留下的各種注釋,下面的圖片就是該類源代碼的一個截圖。
這些注釋的內容非常有參考價值,建議有能力的讀者朋友可以自己閱讀一遍。下面,我們就一步步地抽絲剝繭,來揭開線程池類ThreadPoolExecutor源代碼的神秘面紗。
控制變量與線程池生命周期在ThreadPoolExecutor類定義的開頭,我們可以看到如下的幾行代碼:
// 控制變量,前3位表示狀態,剩下的數據位表示有效的線程數 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // Integer的位數減去3位狀態位就是線程數的位數 private static final int COUNT_BITS = Integer.SIZE - 3; // CAPACITY就是線程數的上限(含),即2^COUNT_BITS - 1個 private static final int CAPACITY = (1 << COUNT_BITS) - 1;
第一行是一個用來作為控制變量的整型值,即一個Integer。之所以要用AtomicInteger類是因為要保證多線程安全,在本系列之后的文章中會對AtomicInteger進行具體介紹。一個整型一般是32位,但是這里的代碼為了保險起見,還是使用了Integer.SIZE來表示整型的總位數。這里的“位”指的是數據位(bit),在計算機中,8bit = 1字節,1024字節 = 1KB,1024KB = 1MB。每一位都是一個0或1的數字,我們如果把整型想象成一個二進制(0或1)的數組,那么一個Integer就是32個數字的數組。其中,前三個被用來表示狀態,那么我們就可以表示2^3 = 8個不同的狀態了。剩下的29位二進制數字都會被用于表示當前線程池中有效線程的數量,上限就是(2^29 - 1)個,即常量CAPACITY。
之后的部分列出了線程池的所有狀態:
private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS;
在這里可以忽略數字后面的<< COUNT_BITS,可以把狀態簡單地理解為前面的數字部分,這樣的簡化基本不影響結論。
各個狀態的解釋如下:
RUNNING,正常運行狀態,可以接受新的任務和處理隊列中的任務
SHUTDOWN,關閉中狀態,不能接受新任務,但是可以處理隊列中的任務
STOP,停止中狀態,不能接受新任務,也不處理隊列中的任務,會中斷進行中的任務
TIDYING,待結束狀態,所有任務已經結束,線程數歸0,進入TIDYING狀態后將會運行terminated()方法
TERMINATED,結束狀態,terminated()方法調用完成后進入
這幾個狀態所對應的數字值是按照順序排列的,也就是說線程池的狀態只能從小到大變化,這也方便了通過數字比較來判斷狀態所在的階段,這種通過數字大小來比較狀態值的方法在ThreadPoolExecutor的源碼中會有大量的使用。
下圖是這五個狀態之間的變化過程:
當線程池被創建時會處于RUNNING狀態,正常接受和處理任務;
當shutdown()方法被直接調用,或者在線程池對象被GC回收時通過finalize()方法隱式調用了shutdown()方法時,線程池會進入SHUTDOWN狀態。該狀態下線程池仍然會繼續執行完阻塞隊列中的任務,只是不再接受新的任務了。當隊列中的任務被執行完后,線程池中的線程也會被回收。當隊列和線程都被清空后,線程池將進入TIDYING狀態;
在線程池處于RUNNING或者SHUTDOWN狀態時,如果有代碼調用了shutdownNow()方法,則線程池會進入STOP狀態。在STOP狀態下,線程池會直接清空阻塞隊列中待執行的任務,然后中斷所有正在進行中的任務并回收線程。當線程都被清空以后,線程池就會進入TIDYING狀態;
當線程池進入TIDYING狀態時,將會運行terminated()方法,該方法執行完后,線程池就會進入最終的TERMINATED狀態,徹底結束。
到這里我們就已經清楚地了解了線程從剛被創建時的RUNNING狀態一直到最終的TERMINATED狀態的整個生命周期了。那么當我們要向一個RUNNING狀態的線程池提交任務時會發生些什么呢?
execute方法的實現我們一般會使用execute方法提交我們的任務,那么線程池在這個過程中做了什么呢?在ThreadPoolExecutor類的execute()方法的源代碼中,我們主要做了四件事:
如果當前線程池中的線程數小于核心線程數corePoolSize,則創建一個新的Worker代表一個線程,并把入參中的任務作為第一個任務傳入Worker。addWorker方法中的第一個參數是該線程的第一個任務,而第二個參數就是代表是否創建的是核心線程,在execute方法中addWorker總共被調用了三次,其中第一次傳入的是true,后兩次傳入的都是false;
如果當前線程池中的線程數已經滿足了核心線程數corePoolSize,那么就會通過workQueue.offer()方法將任務添加到阻塞隊列中等待執行;
如果線程數已經達到了corePoolSize且阻塞隊列中無法插入該任務(比如已滿),那么線程池就會再增加一個線程來執行該任務,除非線程數已經達到了最大線程數maximumPoolSize;
如果確實已經達到了最大線程數,那么就拒絕這個任務。
總體上的執行流程如下,下方的黑色同心圓代表流程結束:
這里再重復一次阻塞隊列的定義,方便大家閱讀:
線程池中的阻塞隊列專門用于存放待執行的任務,在ThreadPoolExecutor中一個任務可以通過兩種方式被執行:第一種是直接在創建一個新的Worker時被作為第一個任務傳入,由這個新創建的線程來執行;第二種就是把任務放入一個阻塞隊列,等待線程池中的工作線程撈取任務進行執行。上面提到的阻塞隊列是這樣的一種數據結構,它是一個隊列(類似于一個List),可以存放0到N個元素。我們可以對這個隊列進行插入和彈出元素的操作,彈出操作可以理解為是一個獲取并從隊列中刪除一個元素的操作。當隊列中沒有元素時,對這個隊列的獲取操作將會被阻塞,直到有元素被插入時才會被喚醒;當隊列已滿時,對這個隊列的插入操作將會被阻塞,直到有元素被彈出后才會被喚醒。這樣的一種數據結構非常適合于線程池的場景,當一個工作線程沒有任務可處理時就會進入阻塞狀態,直到有新任務提交后才被喚醒。
下面是帶有注釋的源代碼,大家可以和上面的流程對照起來參考一下:
public void execute(Runnable command) { // 檢查提交的任務是否為空 if (command == null) throw new NullPointerException(); // 獲取控制變量值 int c = ctl.get(); // 檢查當前線程數是否達到了核心線程數 if (workerCountOf(c) < corePoolSize) { // 未達到核心線程數,則創建新線程 // 并將傳入的任務作為該線程的第一個任務 if (addWorker(command, true)) // 添加線程成功則直接返回,否則繼續執行 return; // 因為前面調用了耗時操作addWorker方法 // 所以線程池狀態有可能發生了改變,重新獲取狀態值 c = ctl.get(); } // 判斷線程池當前狀態是否是運行中 // 如果是則調用workQueue.offer方法將任務放入阻塞隊列 if (isRunning(c) && workQueue.offer(command)) { // 因為執行了耗時操作“放入阻塞隊列”,所以重新獲取狀態值 int recheck = ctl.get(); // 如果當前狀態不是運行中,則將剛才放入阻塞隊列的任務拿出,如果拿出成功,則直接拒絕這個任務 if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) // 如果線程池中沒有線程了,那就創建一個 addWorker(null, false); } // 如果放入阻塞隊列失敗(如隊列已滿),則添加一個線程 else if (!addWorker(command, false)) // 如果添加線程失敗(如已經達到了最大線程數),則拒絕任務 reject(command); }addWorker方法
在前面execute方法的代碼中我們可以看到線程池是通過addWorker方法來向線程池中添加新線程的,那么新的線程又是如何運行起來的呢?
這里我們暫時跳過addWorker方法的詳細源代碼,因為雖然這個方法的代碼行數較多,但是功能相對比較直接,只是創建一個代表線程的Worker類對象,并調用這個對象所對應線程對象的start()方法。我們知道一旦調用了Thread類的start()方法,則這個線程就會開始調用創建線程時傳入的Runnable對象。從下面的Worker類構造器源代碼可以看出,Worker類正是把自己(this指針)傳入了線程的構造器當中,那么這個線程就會運行Worker類的run()方法了,這個run()方法只執行了一行很簡單的代碼runWorker(this);。
Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } public void run() { runWorker(this); }runWorker方法的實現
我們看到線程池中的線程在啟動時會調用對應的Worker類的runWorker方法,而這里就是整個線程池任務執行的核心所在了。runWorker方法中包含有一個類似無限循環的while語句,讓worker對象可以不斷執行提交到線程池中的新任務。
大家可以配合代碼上帶有的注釋來理解該方法的具體實現:
final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; // 將worker的狀態重置為正常狀態,因為state狀態值在構造器中被初始化為-1 w.unlock(); // 通過completedAbruptly變量的值判斷任務是否正常執行完成 boolean completedAbruptly = true; try { // 如果task為null就通過getTask方法獲取阻塞隊列中的下一個任務 // getTask方法一般不會返回null,所以這個while類似于一個無限循環 // worker對象就通過這個方法的持續運行來不斷處理新的任務 while (task != null || (task = getTask()) != null) { // 每一次任務的執行都必須獲取鎖來保證下方臨界區代碼的線程安全 w.lock(); // 如果狀態值大于等于STOP(狀態值是有序的,即STOP、TIDYING、TERMINATED) // 且當前線程還沒有被中斷,則主動中斷線程 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); // 開始 try { // 執行任務前處理操作,默認是一個空實現 // 在子類中可以通過重寫來改變任務執行前的處理行為 beforeExecute(wt, task); // 通過thrown變量保存任務執行過程中拋出的異常 // 提供給下面finally塊中的afterExecute方法使用 Throwable thrown = null; try { // *** 重要:實際執行任務的代碼 task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { // 因為Runnable接口的run方法中不能拋出Throwable對象 // 所以要包裝成Error對象拋出 thrown = x; throw new Error(x); } finally { // 執行任務后處理操作,默認是一個空實現 // 在子類中可以通過重寫來改變任務執行后的處理行為 afterExecute(task, thrown); } } finally { // 將循環變量task設置為null,表示已處理完成 task = null; // 累加當前worker已經完成的任務數 w.completedTasks++; // 釋放while體中第一行獲取的鎖 w.unlock(); } } // 將completedAbruptly變量設置為false,表示任務正常處理完成 completedAbruptly = false; } finally { // 銷毀當前的worker對象,并完成一些諸如完成任務數量統計之類的輔助性工作 // 在線程池當前狀態小于STOP的情況下會創建一個新的worker來替換被銷毀的worker processWorkerExit(w, completedAbruptly); } }
在runWorker方法的源代碼中有兩個比較重要的方法調用,一個是while條件中對getTask方法的調用,一個是在方法的最后對processWorkerExit方法的調用。下面是對這兩個方法更詳細的解釋。
getTask方法在阻塞隊列中有待執行的任務時會從隊列中彈出一個任務并返回,如果阻塞隊列為空,那么就會阻塞等待新的任務提交到隊列中直到超時(在一些配置下會一直等待而不超時),如果在超時之前獲取到了新的任務,那么就會將這個任務作為返回值返回。
當getTask方法返回null時會導致當前Worker退出,當前線程被銷毀。在以下情況下getTask方法才會返回null:
當前線程池中的線程數超過了最大線程數。這是因為運行時通過調用setMaximumPoolSize修改了最大線程數而導致的結果;
線程池處于STOP狀態。這種情況下所有線程都應該被立即回收銷毀;
線程池處于SHUTDOWN狀態,且阻塞隊列為空。這種情況下已經不會有新的任務被提交到阻塞隊列中了,所以線程應該被銷毀;
線程可以被超時回收的情況下等待新任務超時。線程被超時回收一般有以下兩種情況:
超出核心線程數部分的線程等待任務超時
允許核心線程超時(線程池配置)的情況下線程等待任務超時
processWorkerExit方法會銷毀當前線程對應的Worker對象,并執行一些累加總處理任務數等輔助操作。但在線程池當前狀態小于STOP的情況下會創建一個新的Worker來替換被銷毀的Worker,有興趣的讀者可以自行參考processWorkerExit方法源代碼。
總結到這里我們的線程池源代碼之旅就結束了,希望大家在看完這篇文章之后能對線程池的使用和運行都有一個大概的印象。為什么說只是有了一個大概的印象呢?因為我覺得很多沒有相關基礎的讀者讀到這里可能還只是對線程池有了一個自己的認識,對其中的一些細節可能還沒有完全捕捉到。所以我建議大家在看完下面的總結之后不妨再返回到文章的開頭多讀幾遍,相信第二遍的閱讀能給大家帶來不一樣的體驗,因為我自己也是在第三次讀ThreadPoolExecutor類的源代碼時才真正打通了其中的一些重要關節的。
在這篇文章中我們從線程池的概念和基本使用方法說起,然后介紹了ThreadPoolExecutor的構造器參數和常用的四種具體配置。最后的一大半篇幅我們一起在TheadPoolExecutor類的源代碼中暢游了一番,了解了從線程池的創建到任務執行的完整執行模型。
引子在瀏覽ThreadPoolExexutor源碼的過程中,有幾個點我們其實并沒有完全說清楚,比如對鎖的加鎖操作、對控制變量的多次獲取、控制變量的AtomicInteger類型。在下一篇文章中,我將會介紹這些以鎖、volatile變量、CAS操作、AQS抽象類為代表的一系列線程同步方法,歡迎感興趣的讀者繼續關注我后續發布的文章~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73762.html
摘要:那么線程池到底是怎么利用類來實現持續不斷地接收提交的任務并執行的呢接下來,我們通過的源代碼來一步一步抽絲剝繭,揭開線程池運行模型的神秘面紗。 在上一篇文章《從0到1玩轉線程池》中,我們了解了線程池的使用方法,以及向線程池中提交任務的完整流程和ThreadPoolExecutor.execute方法的源代碼。在這篇文章中,我們將會從頭閱讀線程池ThreadPoolExecutor類的源代...
摘要:另外,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為線程私有的內存。運行時常量池運行時常量池是方法區的一部分。 寫在前面(常見面試題) 基本問題: 介紹下 Java 內存區域(運行時數據區) Java 對象的創建過程(五步,建議能默寫出來并且要知道每一步虛擬機做了什么) 對象的訪問定位的兩種方式(句...
摘要:新生代收集器,復制算法,并行收集,面向吞吐量要求吞吐量優先收集器。吞吐量用戶代碼運行時間用戶代碼運行時間垃圾回收時間控制最大垃圾收集停頓時間,大于零的毫秒數。吞吐量大小,到的整數,垃圾收集時間占總時間的比例,計算時間占用比例。 基礎背景 運行時數據區域 虛擬機結構圖 showImg(https://segmentfault.com/img/bVbpRUI?w=378&h=309); 程...
摘要:不同的是它還多了內部類和內部類,以及讀寫對應的成員變量和方法。另外是給和內部類使用的。內部類前面說到的操作是分配到里面執行的。他們都是接口的實現,所以其實最像應該是這個兩個內部類。而且大體上也沒什么差異,也是用的內部類。 之前講了《AQS源碼閱讀》和《ReentrantLock源碼閱讀》,本次將延續閱讀下ReentrantReadWriteLock,建議沒看過之前兩篇文章的,先大概了解...
閱讀 1708·2023-04-26 01:02
閱讀 4870·2021-11-24 09:39
閱讀 1807·2019-08-30 15:44
閱讀 2891·2019-08-30 11:10
閱讀 1787·2019-08-30 10:49
閱讀 988·2019-08-29 17:06
閱讀 611·2019-08-29 16:15
閱讀 908·2019-08-29 15:17