線程池

NO IMAGE

線程池

簡單來說使用線程池有以下幾個目的:
  • 線程是稀缺資源,不能頻繁的創建。
  • 解耦作用;線程的創建於執行完全分開,方便維護。
  • 應當將其放入一個池子中,可以給其他任務進行復用。

使用線程池的好處

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要的等到線程創建就能立即執行。
  • 提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
new Thread的弊端:
– 新建和銷燬線程對象都比較好資源,比性能差。
– 線程缺乏統一管理,可能無限制新建線程,相互之間競爭,及可能佔用過多系統資源導致死機或oom。
– 缺乏更多功能,如定時執行、定期執行、線程中斷。
相比new Thread,Java提供的四種線程池的好處在於:
– 重用存在的線程,減少對象創建、消亡的開銷,性能佳。
– 可有效控制最大併發線程數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。
– 提供定時執行、定期執行、單線程、併發數控制等功能。

線程池原理

談到線程池就會想到池化技術,其中最核心的思想就是把寶貴的資源放到一個池子中;每次使用都從裡面獲取,用完之後又放回池子供其他人使用,有點吃大鍋飯的意思。
那在 Java 中又是如何實現的呢?
在 JDK 1.5 之後推出了相關的 api,常見的創建線程池方式有以下幾種:
  • Executors.newCachedThreadPool():創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程,是一個無限線程池。
  • Executors.newFixedThreadPool(nThreads):創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
  • Executors.newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
  • Executors.newSingleThreadExecutor():創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
ExecutorService基於池化的線程來執行用戶提交的任務,通常可以簡單的通過Executors提供的工廠方法來創建ThreadPoolExecutor實例。
線程池解決的兩個問題:
1)線程池通過減少每次做任務的時候產生的性能消耗來優化執行大量的異步任務的時候的系統性能。
2)線程池還提供了限制和管理批量任務被執行的時候消耗的資源、線程的方法。另外ThreadPoolExecutor還提供了簡單的統計功能,比如當前有多少任務被執行完了。

如果上面的方法創建的實例不能滿足我們的需求,實際開發中也不建議使用Executors工具來創建線程池,我們可以自己通過參數來配置ThreadPoolExecutor,實例化一個線程池實例。

其實看這三種方式創建的源碼就會發現:

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
實際上還是利用 ThreadPoolExecutor 類實現的。
所以我們重點來看下 ThreadPoolExecutor 是怎麼玩的。
首先是創建線程的 api:

ThreadPoolExecutor(int corePoolSize, 
int maximumPoolSize, 
long keepAliveTime, 
TimeUnit unit, 
BlockingQueue<Runnable> workQueue, 
RejectedExecutionHandler handler)

這幾個核心參數的作用:

  • corePoolSize 為線程池的基本大小。
  • maximumPoolSize 為線程池最大線程大小。
  • keepAliveTime 和 unit 則是線程空閒後的存活時間。
  • workQueue 用於存放任務的阻塞隊列。
  • handler 當隊列和最大線程池都滿了之後的飽和策略。

如何配置線程

流程聊完了再來看看上文提到了幾個核心參數應該如何配置呢?
有一點是肯定的,線程池肯定是不是越大越好。
通常我們是需要根據這批任務執行的性質來確定的。
  • IO 密集型任務:由於線程並不是一直在運行,所以可以儘可能的多配置線程,比如 CPU 個數 * 2
  • CPU 密集型任務(大量複雜的運算)應當分配較少的線程,比如 CPU 個數相當的大小。
當然這些都是經驗值,最好的方式還是根據實際情況測試得出最佳配置。
瞭解了這幾個參數再來看看實際的運用。
通常我們都是使用:

threadPool.execute(new Job());
這樣的方式來提交一個任務到線程池中,所以核心的邏輯就是 execute() 函數了。

線程池的狀態

在具體分析之前先了解下線程池中所定義的狀態,這些狀態都和線程的執行密切相關:

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;
  • RUNNING 自然是運行狀態,指可以接受任務執行隊列裡的任務
  • SHUTDOWN 指調用了 shutdown() 方法,不再接受新任務了,但是隊列裡的任務得執行完畢。
  • STOP 指調用了 shutdownNow() 方法,不再接受新任務,同時拋棄阻塞隊列裡的所有任務並中斷所有正在執行任務。
  • TIDYING 所有任務都執行完畢,在調用 shutdown()/shutdownNow() 中都會嘗試更新為這個狀態。
  • TERMINATED 終止狀態,當執行 terminated() 後會更新為這個狀態。
用圖表示為:

線程池

然後看看 execute() 方法是如何處理的:

線程池

  1. 獲取當前線程池的狀態。
  2. 當前線程數量小於 coreSize 時創建一個新的線程運行。
  3. 如果當前線程處於運行狀態,並且寫入阻塞隊列成功。
  4. 雙重檢查,再次獲取線程狀態;如果線程狀態變了(非運行狀態)就需要從阻塞隊列移除任務,並嘗試判斷線程是否全部執行完畢。同時執行拒絕策略。
  5. 如果當前線程池為空就新創建一個線程並執行。
  6. 如果在第三步的判斷為非運行狀態,嘗試新建線程,如果失敗則執行拒絕策略。

線程池的處理流程

ThreadPoolExecutor會根據corePoolSize和maximumPoolSize來動態調整線程池的大小:poolSize。
當任務通過executor提交給線程池的時候:
  • 如果這個時候當前池子中的工作線程數小於corePoolSize,則新創建一個新的工作線程來執行這個任務,不管工作線程集合中有沒有線程是處於空閒狀態。
  • 如果池子中有比corePoolSize大的但是比maximumPoolSize小的工作線程,任務會首先被嘗試著放入隊列,這裡有兩種情況需要單獨說一下:a、如果任務被成功的放入隊列,則看看是否需要開啟新的線程來執行任務,只有噹噹前工作線程數為0的時候才會創建新的線程,因為之前的線程有可能因為都處於空閒狀態或因為工作結束而被移除。b、如果放入隊列失敗,則才會去創建新的工作線程。
  • 如果corePoolSize和maximumPoolSize相同,則線程池的大小是固定的。
通過將maximumPoolSize設置為無限大,我們可以得到一個無上限的線程池。
除了通過構造參數設置這幾個線程池參數之外我們還可以在運行時設置。

線程池

優雅的關閉線程池

當線程池不在被引用並且工作線程數為0的時候,線程池將被終止。從上文提到的 5 個狀態就能看出如何來關閉線程池。
其實無非就是兩個方法 shutdown() / shutdownNow()。(如果我們忘記調用 shutdown,為了讓線程資源被釋放,我們還可以使用keepAliveTime和allowCoreThreadTimeOut來達到目的。)
但他們有著重要的區別:
  • shutdown() 執行後停止接受新任務,會把隊列的任務執行完畢。
  • shutdownNow() 也是停止接受新任務,但會中斷所有的任務,將線程池狀態變為 stop。
兩個方法都會中斷線程,用戶可自行判斷是否需要響應中斷。
shutdownNow() 要更簡單粗暴,可以根據實際場景選擇不同的方法。
我通常是按照以下方式關閉線程池的:

long start = System.currentTimeMillis();
for (int i = 0; i <= 5; i++) {
pool.execute(new Job());
}
pool.shutdown();
while (!pool.awaitTermination(1, TimeUnit.SECONDS)) {
LOGGER.info("線程還在執行。。。");
}
long end = System.currentTimeMillis();
LOGGER.info("一共處理了【{}】", (end - start));
pool.awaitTermination(1, TimeUnit.SECONDS) 會每隔一秒鐘檢查一次是否執行完畢(狀態為 TERMINATED),當從 while 循環退出時就表明線程池已經完全終止了。

線程池隔離

線程池看似很美好,但也會帶來一些問題。
如果我們很多業務都依賴於同一個線程池,當其中一個業務因為各種不可控的原因消耗了所有的線程,導致線程池全部佔滿。這樣其他的業務也就不能正常運轉了,這對系統的打擊是巨大的。
比如我們 Tomcat 接受請求的線程池,假設其中一些響應特別慢,線程資源得不到回收釋放;線程池慢慢被佔滿,最壞的情況就是整個應用都不能提供服務。
所以我們需要將線程池進行隔離。通常的做法是按照業務進行劃分:
比如下單的任務用一個線程池,獲取數據的任務用另一個線程池。這樣即使其中一個出現問題把線程池耗盡,那也不會影響其他的任務運行。

線程調度策略

(1) 搶佔式調度策略

Java運行時系統的線程調度算法是搶佔式的。Java運行時系統支持一種簡單的固定優先級的調度算法。如果一個優先級比其他任何處於可運行狀態的線程都高的線程進入就緒狀態,那麼運行時系統就會選擇該線程運行。新的優先級較高的線程搶佔了其他線程。但是Java運行時系統並不搶佔同優先級的線程。換句話說,Java運行時系統不是分時的。然而,基於Java Thread類的實現系統可能是支持分時的,因此編寫代碼時不要依賴分時。當系統中的處於就緒狀態的線程都具有相同優先級時,線程調度程序採用一種簡單的、非搶佔式的輪轉的調度順序。

(2) 時間片輪轉調度策略

有些系統的線程調度採用時間片輪轉調度策略。這種調度策略是從所有處於就緒狀態的線程中選擇優先級最高的線程分配一定的CPU時間運行。該時間過後再選擇其他線程運行。只有當線程運行結束、放棄(yield)CPU或由於某種原因進入阻塞狀態,低優先級的線程才有機會執行。如果有兩個優先級相同的線程都在等待CPU,則調度程序以輪轉的方式選擇運行的線程。

總結

線程池這塊的知識的確博大精深,我也還有待繼續深入學習。

參考自:crossoverjie.top/JCSprout/#/…

相關文章

事務ACID特性與隔離級別

聊聊J.U.CAQS

詳解CyclicBarrier

詳解CountDownLatch