帶著問題
* 阿里Java代碼規(guī)范為什么不允許使用Executors快速創(chuàng)建線程池?
* 下面的代碼輸出是什么? ThreadPoolExecutor executor = new ThreadPoolExecutor( 1,
//corePoolSize 100, //maximumPoolSize 100, //keepAliveTime TimeUnit.SECONDS,
//unit new LinkedBlockingDeque<>(100));//workQueue for (int i = 0; i < 5; i++)
{ final int taskIndex = i; executor.execute(() -> {
System.out.println(taskIndex); try { Thread.sleep(Long.MAX_VALUE); } catch
(InterruptedException e) { e.printStackTrace(); } }); }
A) 0 1 2 3 4 5
B) 0~5 順序不一致輸出5行
C) 0
基礎(chǔ)
什么是線程池?
線程池可以通過池看出來是一個(gè)資源集,任何池的作用都大同小異,主要是用來減少資源創(chuàng)建、初始化的系統(tǒng)開銷。
創(chuàng)建線程很“貴”嗎?
是的。創(chuàng)建線程的代價(jià)是昂貴的。
我們都知道系統(tǒng)中的每個(gè)進(jìn)程有自己獨(dú)立的內(nèi)存空間,而被稱為輕量級進(jìn)程的線程也是需要的。
在JVM中默認(rèn)一個(gè)線程需要使用256k~1M(取決于32位還是64位操作系統(tǒng))的內(nèi)存。(具體的數(shù)組我們不深究,因?yàn)殡S著JVM版本的變化這個(gè)默認(rèn)值隨時(shí)可能發(fā)生變更,我們只需要知道線程是需要占用內(nèi)存的)
除了內(nèi)存還有更多嗎?
許多文章會將上下文切換、CPU調(diào)度列入其中,這邊不將線程調(diào)度列入是因?yàn)樗咧械木€程不會被調(diào)度(OS控制),如果不是睡眠中的線程那么是一定需要被調(diào)度的。
但在JVM中除了創(chuàng)建時(shí)的內(nèi)存消耗,還會給GC帶來壓力,如果頻繁創(chuàng)建線程那么相對的GC的時(shí)候也需要回收對應(yīng)的線程。
線程池的機(jī)制?
可以看到線程池是一種重復(fù)利用線程的技術(shù),線程池的主要機(jī)制就是保留一定的線程數(shù)在沒有事情做的時(shí)候使之睡眠,當(dāng)有活干的時(shí)候拿一個(gè)線程去運(yùn)行。
這些牽扯到線程池實(shí)現(xiàn)的具體策略。
還有哪些常見的池?
* 線程池
* 連接池(數(shù)據(jù)庫連接、TCP連接等)
* BufferPool
* ......
Java中的線程池
UML圖(Java 8)
可以看到真正的實(shí)現(xiàn)類有
* ThreadPoolExecutor (1.5)
* ForkJoinPool (1.7)
* ScheduledThreadPoolExecutor (1.5)
今天我們主要談?wù)?ThreadPoolExecutor 也是使用率較高的一個(gè)實(shí)現(xiàn)。
Executors提供的工廠方法
*
newCachedThreadPool (ThreadPoolExecutor)
創(chuàng)建一個(gè)可緩存的線程池。如果線程池的大小超過了處理任務(wù)所需要的線程,那么就會回收部分空閑(60秒不執(zhí)行任務(wù))的線程,當(dāng)任務(wù)數(shù)增加時(shí),此線程池又可以智能的添加新線程來處理任務(wù)。此線程池不會對線程池大小做限制,線程池大小完全依賴于操作系統(tǒng)(或者說JVM)能夠創(chuàng)建的最大線程大小。
*
newFixedThreadPool (ThreadPoolExecutor)
創(chuàng)建固定大小的線程池。每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到線程達(dá)到線程池的最大大小。線程池的大小一旦達(dá)到最大值就會保持不變,如果某個(gè)線程因?yàn)閳?zhí)行異常而結(jié)束,那么線程池會補(bǔ)充一個(gè)新線程。
*
newSingleThreadExecutor (ThreadPoolExecutor)
創(chuàng)建一個(gè)單線程的線程池。這個(gè)線程池只有一個(gè)線程在工作,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù)。如果這個(gè)唯一的線程因?yàn)楫惓=Y(jié)束,那么會有一個(gè)新的線程來替代它。此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行。
*
newScheduledThreadPool (ScheduledThreadPoolExecutor)
創(chuàng)建一個(gè)大小無限的線程池。此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求。
*
newSingleThreadScheduledExecutor (ScheduledThreadPoolExecutor)
創(chuàng)建一個(gè)單線程用于定時(shí)以及周期性執(zhí)行任務(wù)的需求。
*
newWorkStealingPool (1.8 ForkJoinPool)
創(chuàng)建一個(gè)工作竊取
可以看到各種不同的工廠方法中使用的線程池實(shí)現(xiàn)類最終只有3個(gè),對應(yīng)關(guān)系如下:
工廠方法 實(shí)現(xiàn)類
newCachedThreadPool ThreadPoolExecutor
newFixedThreadPool ThreadPoolExecutor
newSingleThreadExecutor ThreadPoolExecutor
newScheduledThreadPool ScheduledThreadPoolExecutor
newSingleThreadScheduledExecutor ScheduledThreadPoolExecutor
newWorkStealingPool ForkJoinPool
ThreadPoolExecutor
首先我們看下 ThreadPoolExecutor 的完全構(gòu)造函數(shù)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long
keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory
threadFactory, RejectedExecutionHandler handler)
*
corePoolSize
核心池大小,除非設(shè)置了 allowCoreThreadTimeOut 否則哪怕線程超過空閑時(shí)間,池中也要最少要保留這個(gè)數(shù)目的線程。
需要注意的是,corePoolSize所需的線程并不是立即創(chuàng)建的,需要在提交任務(wù)之后進(jìn)行創(chuàng)建,所以如果有大量的緩存線程數(shù)可以先提交一個(gè)空任務(wù)讓線程池將線程先創(chuàng)建出來,從而提升后續(xù)的執(zhí)行效率。
*
maximumPoolSize
允許的最大線程數(shù)。
*
keepAliveTime
空閑線程空閑存活時(shí)間,核心線程需要 allowCoreThreadTimeOut 為true才會退出。
*
unit
與 keepAliveTime 配合,設(shè)置 keepAliveTime 的單位,如:毫秒、秒。
*
workQueue
線程池中的任務(wù)隊(duì)列。上面提到線程池的主要作用是復(fù)用線程來處理任務(wù),所以我們需要一個(gè)隊(duì)列來存放需要執(zhí)行的任務(wù),在使用池中的線程來處理這些任務(wù),所以我們需要一個(gè)任務(wù)隊(duì)列。
*
threadFactory
當(dāng)線程池判斷需要新的線程時(shí)通過線程工程創(chuàng)建線程。
*
handler
執(zhí)行被阻止時(shí)的處理程序,線程池?zé)o法處理。這個(gè)與任務(wù)隊(duì)列相關(guān),比如隊(duì)列中可以指定隊(duì)列大小,如果超過了這個(gè)大小該怎么辦呢?JDK已經(jīng)為我們考慮到了,并提供了4個(gè)默認(rèn)實(shí)現(xiàn)。
下列是JDK中默認(rèn)攜帶的策略:
*
AbortPolicy (默認(rèn))
拋出 RejectedExecutionException 異常。
*
CallerRunsPolicy
調(diào)用當(dāng)前線程池所在的線程去執(zhí)行。
*
DiscardPolicy
直接丟棄當(dāng)前任務(wù)。
*
DiscardOldestPolicy
將最舊的任務(wù)丟棄,將當(dāng)前任務(wù)添加到隊(duì)列。
容易混淆的參數(shù):corePoolSize maximumPoolSize workQueue
任務(wù)隊(duì)列、核心線程數(shù)、最大線程數(shù)的邏輯關(guān)系
* 當(dāng)線程數(shù)小于核心線程數(shù)時(shí),創(chuàng)建線程。
* 當(dāng)線程數(shù)大于等于核心線程數(shù),且任務(wù)隊(duì)列未滿時(shí),將任務(wù)放入任務(wù)隊(duì)列。
* 當(dāng)線程數(shù)大于等于核心線程數(shù),且任務(wù)隊(duì)列已滿
* 若線程數(shù)小于最大線程數(shù),創(chuàng)建線程
* 若線程數(shù)等于最大線程數(shù),調(diào)用拒絕執(zhí)行處理程序(默認(rèn)效果為:拋出異常,拒絕任務(wù))
那么這三個(gè)參數(shù)推薦如何設(shè)置,有最優(yōu)值嗎?
由于java對于協(xié)程的支持不友好,所以會大量依賴于線程池和線程。
從而這個(gè)值沒有最優(yōu)推薦,需要根據(jù)業(yè)務(wù)需求情況來進(jìn)行設(shè)置。
不同的需求類型可以創(chuàng)建多個(gè)不同的線程池來執(zhí)行。
問題1:阿里開發(fā)規(guī)范為什么不允許Executors快速創(chuàng)建線程池?
參考地址:https://github.com/alibaba/p3c <https://github.com/alibaba/p3c>
可以看到原因很簡單
* newSingleThreadExecutor
* newFixedThreadPool
在 workQueue 參數(shù)直接 使用了 new LinkedBlockingQueue<Runnable>() 理論上可以無限添加任務(wù)到線程池。
public static ExecutorService newFixedThreadPool(int nThreads) { return new
ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new
LinkedBlockingQueue<Runnable>(); } public static ExecutorService
newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService(new
ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new
LinkedBlockingQueue<Runnable>())); }
如果提交到線程池的任務(wù)由問題,比如 sleep 永久,會造成內(nèi)存泄漏,最終導(dǎo)致OOM。
同時(shí) 阿里還推薦自定義 threadFactory 設(shè)置線程名稱便于以后排查問題。
問題2:下面的代碼輸出是什么?
應(yīng)該選C。
雖然最大線程數(shù)有100但核心線程數(shù)為1,任務(wù)隊(duì)列由100。
滿足了 '當(dāng)線程數(shù)大于等于核心線程數(shù),且任務(wù)隊(duì)列未滿時(shí),將任務(wù)放入任務(wù)隊(duì)列。' 這個(gè)條件。
所以后續(xù)添加的任務(wù)都會被堵塞。
最后
關(guān)于 ThreadPoolExecutor
的邏輯在實(shí)際使用的時(shí)候會有點(diǎn)奇怪,因?yàn)榫€程池中的線程并沒有超過最大線程數(shù),有沒有一種可能當(dāng)任務(wù)被堵塞很久的時(shí)候創(chuàng)建新的線程池來處理呢?
這邊推薦大家使用 newWorkStealingPool,也就是ForkJoinPool。采取了工作竊取的模式。
后續(xù)會跟大家一起聊聊 ForkJoinPool。
熱門工具 換一換