本文目錄一覽:
合理使用線程池以及線程變數
背景
隨著計算技術的不斷發展,3納米製程晶元已進入試產階段,摩爾定律在現有工藝下逐漸面臨巨大的物理瓶頸,通過多核處理器技術來提升伺服器的性能成為提升算力的主要方向。
在伺服器領域,基於java構建的後端伺服器佔據著領先地位,因此,掌握java並發編程技術,充分利用CPU的並發處理能力是一個開發人員必修的基本功,本文結合線程池源碼和實踐,簡要介紹了線程池和線程變數的使用。
線程池概述
線程池是一種「池化」的線程使用模式,通過創建一定數量的線程,讓這些線程處於就緒狀態來提高系統響應速度,在線程使用完成後歸還到線程池來達到重複利用的目標,從而降低系統資源的消耗。
總體來說,線程池有如下的優勢:
線程池的使用
在java中,線程池的實現類是ThreadPoolExecutor,構造函數如下:
可以通過 new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,handler)來創建一個線程池。
在構造函數中,corePoolSize為線程池核心線程數。默認情況下,核心線程會一直存活,但是當將allowCoreThreadTimeout設置為true時,核心線程超時也會回收。
在構造函數中,maximumPoolSize為線程池所能容納的最大線程數。
在構造函數中,keepAliveTime表示線程閑置超時時長。如果線程閑置時間超過該時長,非核心線程就會被回收。如果將allowCoreThreadTimeout設置為true時,核心線程也會超時回收。
在構造函數中,timeUnit表示線程閑置超時時長的時間單位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
在構造函數中,blockingQueue表示任務隊列,線程池任務隊列的常用實現類有:
在構造函數中,threadFactory表示線程工廠。用於指定為線程池創建新線程的方式,threadFactory可以設置線程名稱、線程組、優先順序等參數。如通過Google工具包可以設置線程池裡的線程名:
在構造函數中,rejectedExecutionHandler表示拒絕策略。當達到最大線程數且隊列任務已滿時需要執行的拒絕策略,常見的拒絕策略如下:
ThreadPoolExecutor線程池有如下幾種狀態:
線程池提交一個任務時任務調度的主要步驟如下:
核心代碼如下:
Tomcat 的整體架構包含連接器和容器兩大部分,其中連接器負責與外部通信,容器負責內部邏輯處理。在連接器中:
Tomcat為了實現請求的快速響應,使用線程池來提高請求的處理能力。下面我們以HTTP非阻塞I/O為例對Tomcat線程池進行簡要的分析。
在Tomcat中,通過AbstractEndpoint類提供底層的網路I/O的處理,若用戶沒有配置自定義公共線程池,則AbstractEndpoint通過createExecutor方法來創建Tomcat默認線程池。
核心部分代碼如下:
其中,TaskQueue、ThreadPoolExecutor分別為Tomcat自定義任務隊列、線程池實現。
Tomcat自定義線程池繼承於java.util.concurrent.ThreadPoolExecutor,並新增了一些成員變數來更高效地統計已經提交但尚未完成的任務數量(submittedCount),包括已經在隊列中的任務和已經交給工作線程但還未開始執行的任務。
Tomcat在自定義線程池ThreadPoolExecutor中重寫了execute()方法,並實現對提交執行的任務進行submittedCount加一。Tomcat在自定義ThreadPoolExecutor中,當線程池拋出RejectedExecutionException異常後,會調用force()方法再次向TaskQueue中進行添加任務的嘗試。如果添加失敗,則submittedCount減一後,再拋出RejectedExecutionException。
在Tomcat中重新定義了一個阻塞隊列TaskQueue,它繼承於LinkedBlockingQueue。在Tomcat中,核心線程數默認值為10,最大線程數默認為200, 為了避免線程到達核心線程數後後續任務放入隊列等待,Tomcat通過自定義任務隊列TaskQueue重寫offer方法實現了核心線程池數達到配置數後線程的創建。
具體地,從線程池任務調度機制實現可知,當offer方法返回false時,線程池將嘗試創建新新線程,從而實現任務的快速響應。TaskQueue核心實現代碼如下:
Tomcat中通過自定義任務線程TaskThread實現對每個線程創建時間的記錄;使用靜態內部類WrappingRunnable對Runnable進行包裝,用於對StopPooledThreadException異常類型的處理。
Executors常用方法有以下幾個:
Executors類看起來功能比較強大、用起來還比較方便,但存在如下弊端 :
使用線程時,可以直接調用 ThreadPoolExecutor 的構造函數來創建線程池,並根據業務實際場景來設置corePoolSize、blockingQueue、RejectedExecuteHandler等參數。
使用局部線程池時,若任務執行完後沒有執行shutdown()方法或有其他不當引用,極易造成系統資源耗盡。
在工程實踐中,通常使用下述公式來計算核心線程數:
nThreads=(w+c)/c*n*u=(w/c+1)*n*u
其中,w為等待時間,c為計算時間,n為CPU核心數(通常可通過 Runtime.getRuntime().availableProcessors()方法獲取),u為CPU目標利用率(取值區間為[0, 1]);在最大化CPU利用率的情況下,當處理的任務為計算密集型任務時,即等待時間w為0,此時核心線程數等於CPU核心數。
上述計算公式是理想情況下的建議核心線程數,而不同系統/應用在運行不同的任務時可能會有一定的差異,因此最佳線程數參數還需要根據任務的實際運行情況和壓測表現進行微調。
為了更好地發現、分析和解決問題,建議在使用多線程時增加對異常的處理,異常處理通常有下述方案:
為了實現優雅停機的目標,我們應當先調用shutdown方法,調用這個方法也就意味著,這個線程池不會再接收任何新的任務,但是已經提交的任務還會繼續執行。之後我們還應當調用awaitTermination方法,這個方法可以設定線程池在關閉之前的最大超時時間,如果在超時時間結束之前線程池能夠正常關閉則會返回true,否則,超時會返回false。通常我們需要根據業務場景預估一個合理的超時時間,然後調用該方法。
如果awaitTermination方法返回false,但又希望儘可能在線程池關閉之後再做其他資源回收工作,可以考慮再調用一下shutdownNow方法,此時隊列中所有尚未被處理的任務都會被丟棄,同時會設置線程池中每個線程的中斷標誌位。shutdownNow並不保證一定可以讓正在運行的線程停止工作,除非提交給線程的任務能夠正確響應中斷。
ThreadLocal線程變數概述
ThreadLocal類提供了線程本地變數(thread-local variables),這些變數不同於普通的變數,訪問線程本地變數的每個線程(通過其get或set方法)都有其自己的獨立初始化的變數副本,因此ThreadLocal沒有多線程競爭的問題,不需要單獨進行加鎖。
ThreadLocal的原理與實踐
對於ThreadLocal而言,常用的方法有get/set/initialValue 3個方法。
眾所周知,在java中SimpleDateFormat有線程安全問題,為了安全地使用SimpleDateFormat,除了1)創建SimpleDateFormat局部變數;和2)加同步鎖 兩種方案外,我們還可以使用3)ThreadLocal的方案:
Thread 內部維護了一個 ThreadLocal.ThreadLocalMap 實例(threadLocals),ThreadLocal 的操作都是圍繞著 threadLocals 來操作的。
從JDK源碼可見,ThreadLocalMap中的Entry是弱引用類型的,這就意味著如果這個ThreadLocal只被這個Entry引用,而沒有被其他對象強引用時,就會在下一次GC的時候回收掉。
EagleEye(鷹眼)作為全鏈路監控系統在集團內部被廣泛使用,traceId、rpcId、壓測標等信息存儲在EagleEye的ThreadLocal變數中,並在HSF/Dubbo服務調用間進行傳遞。EagleEye通過Filter將數據初始化到ThreadLocal中,部分相關代碼如下:
在EagleEyeFilter中,通過EagleEyeRequestTracer.startTrace方法進行初始化,在前置入參轉換後,通過startTrace重載方法將鷹眼上下文參數存入ThreadLocal中,相關代碼如下:
EagleEyeFilter在finally代碼塊中,通過EagleEyeRequestTracer.endTrace方法結束調用鏈,通過clear方法將ThreadLocal中的數據進行清理,相關代碼實現如下:
在某權益領取原有鏈路中,通過app打開一級頁面後才能發起權益領取請求,請求經過淘系無線網關(Mtop)後到達服務端,服務端通過mtop sdk獲取當前會話信息。
在XX項目中,對權益領取鏈路進行了升級改造,在一級頁面請求時,通過服務端同時發起權益領取請求。具體地,服務端在處理一級頁面請求時,同時通過調用hsf/dubbo介面來進行權益領取,因此在發起rpc調用時需要攜帶用戶當前會話信息,在服務提供端將會話信息進行提取並注入到mtop上下文,從而才能通過mtop sdk獲取到會話id等信息。某開發同學在實現時,因ThreadLocal使用不當造成下述問題:
【問題1:權益領取失敗分析】
在權益領取服務中,該應用構建了一套高效和線程安全的依賴注入框架,基於該框架的業務邏輯模塊通常抽象為xxxModule形式,Module間為網狀依賴關係,框架會按依賴關係自動調用init方法(其中,被依賴的module 的init方法先執行)。
在應用中,權益領取介面的主入口為CommonXXApplyModule類,CommonXXApplyModule依賴XXSessionModule。當請求來臨時,會按依賴關係依次調用init方法,因此XXSessionModule的init方法會優先執行;而開發同學在CommonXXApplyModule類中的init方法中通過調用recoverMtopContext()方法來期望恢復mtop上下文,因recoverMtopContext()方法的調用時機過晚,從而導致XXSessionModule模塊獲取不到正確的會話id等信息而導致權益領取失敗。
【問題2:臟數據分析】
權益領取服務在處理請求時,若當前線程曾經處理過權益領取請求,因ThreadLocal變數值未被清理,此時XXSessionModule通過mtop SDK獲取會話信息時得到的是前一次請求的會話信息,從而造成臟數據。
【解決方案】
在依賴注入框架入口處AbstractGate#visit(或在XXSessionModule中)通過recoverMtopContext方法注入mtop上下文信息,並在入口方法的finally代碼塊清理當前請求的threadlocal變數值。
若使用強引用類型,則threadlocal的引用鏈為:Thread – ThreadLocal.ThreadLocalMap – Entry[] – Entry – key(threadLocal對象)和value;在這種場景下,只要這個線程還在運行(如線程池場景),若不調用remove方法,則該對象及關聯的所有強引用對象都不會被垃圾回收器回收。
若使用static關鍵字進行修飾,則一個線程僅對應一個線程變數;否則,threadlocal語義變為perThread-perInstance,容易引發內存泄漏,如下述示例:
在上述main方法第22行debug,可見線程的threadLocals變數中有3個threadlocal實例。在工程實踐中,使用threadlocal時通常期望一個線程只有一個threadlocal實例,因此,若不使用static修飾,期望的語義發生了變化,同時易引起內存泄漏。
如果不執行清理操作,則可能會出現:
建議使用try…finally 進行清理。
我們在使用ThreadLocal時,通常期望的語義是perThread,若不使用static進行修飾,則語義變為perThread-perInstance;在線程池場景下,若不用static進行修飾,創建的線程相關實例可能會達到 M * N個(其中M為線程數,N為對應類的實例數),易造成內存泄漏()。
在應用中,謹慎使用ThreadLocal.withInitial(Supplier? extends S supplier)這個工廠方法創建ThreadLocal對象,一旦不同線程的ThreadLocal使用了同一個Supplier對象,那麼隔離也就無從談起了,如:
總結
在java工程實踐中,線程池和線程變數被廣泛使用,因線程池和線程變數的不當使用經常造成安全生產事故,因此,正確使用線程池和線程變數是每一位開發人員必須修鍊的基本功。本文從線程池和線程變數的使用出發,簡要介紹了線程池和線程變數的原理和使用實踐,各開發人員可結合最佳實踐和實際應用場景,正確地使用線程和線程變數,構建出穩定、高效的java應用服務。
java線程池怎麼實現
要想理解清楚java線程池實現原理,明白下面幾個問題就可以了:
(1):線程池存在哪些狀態,這些狀態之間是如何進行切換的呢?
(2):線程池的種類有哪些?
(3):創建線程池需要哪些參數,這些參數的具體含義是什麼?
(4):將任務添加到線程池之後運行流程?
(5):線程池是怎麼做到重用線程的呢?
(6):線程池的關閉
首先回答第一個問題:線程池存在哪些狀態;
查看ThreadPoolExecutor源碼便知曉:
[java]view plaincopy
//runStateisstoredinthehigh-orderbits
privatestaticfinalintRUNNING=-1COUNT_BITS;
privatestaticfinalintSHUTDOWN=0COUNT_BITS;
privatestaticfinalintSTOP=1COUNT_BITS;
privatestaticfinalintTIDYING=2COUNT_BITS;
privatestaticfinalintTERMINATED=3COUNT_BITS;
存在5種狀態:
1Running:可以接受新任務,同時也可以處理阻塞隊列裡面的任務;
2Shutdown:不可以接受新任務,但是可以處理阻塞隊列裡面的任務;
3Stop:不可以接受新任務,也不處理阻塞隊列裡面的任務,同時還中斷正在處理的任務;
4Tidying:屬於過渡階段,在這個階段表示所有的任務已經執行結束了,當前線程池中是不存在有效的線程的,並且將要調用terminated方法;
5Terminated:終止狀態,這個狀態是在調用完terminated方法之後所處的狀態;
那麼這5種狀態之間是如何進行轉換的呢?查看ThreadPoolExecutor源碼裡面的注釋便可以知道啦:
[java]view plaincopy
*RUNNING-SHUTDOWN
*Oninvocationofshutdown(),perhapsimplicitlyinfinalize()
*(RUNNINGorSHUTDOWN)-STOP
*OninvocationofshutdownNow()
*SHUTDOWN-TIDYING
*Whenbothqueueandpoolareempty
*STOP-TIDYING
*Whenpoolisempty
*TIDYING-TERMINATED
*Whentheterminated()hookmethodhascompleted
從上面可以看到,在調用shutdown方法的時候,線程池狀態會從Running轉換成Shutdown;在調用shutdownNow方法的時候,線程池狀態會從Running/Shutdown轉換成Stop;在阻塞隊列為空同時線程池為空的情況下,線程池狀態會從Shutdown轉換成Tidying;在線程池為空的情況下,線程池狀態會從Stop轉換成Tidying;當調用terminated方法之後,線程池狀態會從Tidying轉換成Terminate;
在明白了線程池的各個狀態以及狀態之間是怎麼進行切換之後,我們來看看第二個問題,線程池的種類:
(1):CachedThreadPool:緩存線程池,該類線程池中線程的數量是不確定的,理論上可以達到Integer.MAX_VALUE個,這種線程池中的線程都是非核心線程,既然是非核心線程,那麼就存在超時淘汰機制了,當裡面的某個線程空閑時間超過了設定的超時時間的話,就會回收掉該線程;
(2):FixedThreadPool:固定線程池,這類線程池中是只存在核心線程的,對於核心線程來說,如果我們不設置allowCoreThreadTimeOut屬性的話是不存在超時淘汰機制的,這類線程池中的corePoolSize的大小是等於maximumPoolSize大小的,也就是說,如果線程池中的線程都處於活動狀態的話,如果有新任務到來,他是不會開闢新的工作線程來處理這些任務的,只能將這些任務放到阻塞隊列裡面進行等到,直到有核心線程空閑為止;
(3):ScheduledThreadPool:任務線程池,這種線程池中核心線程的數量是固定的,而對於非核心線程的數量是不限制的,同時對於非核心線程是存在超時淘汰機制的,主要適用於執行定時任務或者周期性任務的場景;
(4):SingleThreadPool:單一線程池,線程池裡面只有一個線程,同時也不存在非核心線程,感覺像是FixedThreadPool的特殊版本,他主要用於確保任務在同一線程中的順序執行,有點類似於進行同步吧;
接下來我們來看第三個問題,創建線程池需要哪些參數:
同樣查看ThreadPoolExecutor源碼,查看創建線程池的構造函數:
[java]view plaincopy
publicThreadPoolExecutor(intcorePoolSize,
intmaximumPoolSize,
longkeepAliveTime,
TimeUnitunit,
BlockingQueueRunnableworkQueue,
ThreadFactorythreadFactory,
RejectedExecutionHandlerhandler)
不管你調用的是ThreadPoolExecutor的哪個構造函數,最終都會執行到這個構造函數的,這個構造函數有7個參數,正是由於對這7個參數值的賦值不同,造成生成不同類型的線程池,比如我們常見的CachedThreadPoolExecutor、FixedThreadPoolExecutor
SingleThreadPoolExecutor、ScheduledThreadPoolExecutor,我們老看看這幾個參數的具體含義:
1corePoolSize:線程池中核心線程的數量;當提交一個任務到線程池的時候,線程池會創建一個線程來執行執行任務,即使有其他空閑的線程存在,直到線程數達到corePoolSize時不再創建,這時候會把提交的新任務放入到阻塞隊列中,如果調用了線程池的preStartAllCoreThreads方法,則會在創建線程池的時候初始化出來核心線程;
2maximumPoolSize:線程池允許創建的最大線程數;如果阻塞隊列已經滿了,同時已經創建的線程數小於最大線程數的話,那麼會創建新的線程來處理阻塞隊列中的任務;
3keepAliveTime:線程活動保持時間,指的是工作線程空閑之後繼續存活的時間,默認情況下,這個參數只有線程數大於corePoolSize的時候才會起作用,即當線程池中的線程數目大於corePoolSize的時候,如果某一個線程的空閑時間達到keepAliveTime,那麼這個線程是會被終止的,直到線程池中的線程數目不大於corePoolSize;如果調用allowCoreThreadTimeOut的話,在線程池中線程數量不大於corePoolSize的時候,keepAliveTime參數也可以起作用的,知道線程數目為0為止;
4unit:參數keepAliveTime的時間單位;
5workQueue:阻塞隊列;用於存儲等待執行的任務,有四種阻塞隊列類型,ArrayBlockingQueue(基於數組的有界阻塞隊列)、LinkedBlockingQueue(基於鏈表結構的阻塞隊列)、SynchronousQueue(不存儲元素的阻塞隊列)、PriorityBlockingQueue(具有優先順序的阻塞隊列);
6threadFactory:用於創建線程的線程工廠;
7handler:當阻塞隊列滿了,且沒有空閑線程的情況下,也就是說這個時候,線程池中的線程數目已經達到了最大線程數量,處於飽和狀態,那麼必須採取一種策略來處理新提交的任務,我們可以自己定義處理策略,也可以使用系統已經提供給我們的策略,先來看看系統為我們提供的4種策略,AbortPolicy(直接拋出異常)、CallerRunsPolicy(只有調用者所在的線程來運行任務)、DiscardOldestPolicy(丟棄阻塞隊列中最近的一個任務,並執行當前任務)、Discard(直接丟棄);
接下來就是將任務添加到線程池之後的運行流程了;
我們可以調用submit或者execute方法,兩者最大的區別在於,調用submit方法的話,我們可以傳入一個實現Callable介面的對象,進而能在當前任務執行結束之後通過Future對象獲得任務的返回值,submit內部實際上還是執行的execute方法;而調用execute方法的話,是不能獲得任務執行結束之後的返回值的;此外,調用submit方法的話是可以拋出異常的,但是調用execute方法的話,異常在其內部得到了消化,也就是說異常在其內部得到了處理,不會向外傳遞的;
因為submit方法最終也是會執行execute方法的,因此我們只需要了解execute方法就可以了:
在execute方法內部會分三種情況來進行處理:
1:首先判斷當前線程池中的線程數量是否小於corePoolSize,如果小於的話,則直接通過addWorker方法創建一個新的Worker對象來執行我們當前的任務;
2:如果說當前線程池中的線程數量大於corePoolSize的話,那麼會嘗試將當前任務添加到阻塞隊列中,然後第二次檢查線程池的狀態,如果線程池不在Running狀態的話,會將剛剛添加到阻塞隊列中的任務移出,同時拒絕當前任務請求;如果第二次檢查發現當前線程池處於Running狀態的話,那麼會查看當前線程池中的工作線程數量是否為0,如果為0的話,就會通過addWorker方法創建一個Worker對象出來處理阻塞隊列中的任務;
3:如果原先線程池就不處於Running狀態或者我們剛剛將當前任務添加到阻塞隊列的時候出現錯誤的話,那麼會去嘗試通過addWorker創建新的Worker來處理當前任務,如果添加失敗的話,則拒絕當前任務請求;
可以看到在上面的execute方法中,我們僅僅只是檢查了當前線程池中的線程數量有沒有超過corePoolSize的情況,那麼當前線程池中的線程數量有沒有超過maximumPoolSize是在哪裡檢測的呢?實際上是在addWorker方法裡面了,我們可以看下addWorker裡面的一段代碼:
[java]view plaincopy
if(wc=CAPACITY||
wc=(core?corePoolSize:maximumPoolSize))
returnfalse;
如果當前線程數量超過maximumPoolSize的話,直接就會調用return方法,返回false;
其實到這裡我們很明顯可以知道,一個線程池中線程的數量實際上就是這個線程池中Worker的數量,如果Worker的大小超過了corePoolSize,那麼任務都在阻塞隊列裡面了,Worker是Java對我們任務的一個封裝類,他的聲明是醬紫的:
[java]view plaincopy
privatefinalclassWorker
extendsAbstractQueuedSynchronizer
implementsRunnable
可以看到他實現了Runnable介面,他是在addWorker方法裡面通過new Worker(firstTask)創建的,我們來看看他的構造函數就知道了:
[java]view plaincopy
Worker(RunnablefirstTask){
setState(-1);//inhibitinterruptsuntilrunWorker
this.firstTask=firstTask;
this.thread=getThreadFactory().newThread(this);
}
而這裡的firstTask其實就是我們調用execute或者submit的時候傳入的那個參數罷了,一般來說這些參數是實現Callable或者Runnable介面的;
在通過addWorker方法創建出來Worker對象之後,這個方法的最後會執行Worker內部thread屬性的start方法,而這個thread屬性實際上就是封裝了Worker的Thread,執行他的start方法實際上執行的是Worker的run方法,因為Worker是實現了Runnable介面的,在run方法裡面就會執行runWorker方法,而runWorker方法裡面首先會判斷當前我們傳入的任務是否為空,不為空的話直接就會執行我們通過execute或者submit方法提交的任務啦,注意一點就是我們雖然會通過submit方法提交實現了Callable介面的對象,但是在調用submit方法的時候,其實是會將Callable對象封裝成實現了Runnable介面對象的,不信我們看看submit方法源碼是怎麼實現的:
[java]view plaincopy
publicTFutureTsubmit(CallableTtask){
if(task==null)thrownewNullPointerException();
RunnableFutureTftask=newTaskFor(task);
execute(ftask);
returnftask;
}
看到沒有呢,實際上在你傳入實現了Callable介面對象的時候,在submit方法裡面是會將其封裝成RunnableFuture對象的,而RunnableFuture介面是繼承了Runnable介面的;那麼說白了其實就是直接執行我們提交任務的run方法了;如果為空的話,則會通過getTask方法從阻塞隊列裡面拿出一個任務去執行;在任務執行結束之後繼續從阻塞隊列裡面拿任務,直到getTask的返回值為空則退出runWorker內部循環,那麼什麼情況下getTask返回為空呢?查看getTask方法的源碼注釋可以知道:在Worker必須需要退出的情況下getTask會返回空,具體什麼情況下Worker會退出呢?(1):當Worker的數量超過maximumPoolSize的時候;(2):當線程池狀態為Stop的時候;(3):當線程池狀態為Shutdown並且阻塞隊列為空的時候;(4):使用等待超時時間從阻塞隊列中拿數據,但是超時之後仍然沒有拿到數據;
如果runWorker方法退出了它裡面的循環,那麼就說明當前阻塞隊列裡面是沒有任務可以執行的了,你可以看到在runWorker方法內部的finally語句塊中執行了processWorkerExit方法,用來對Worker對象進行回收操作,這個方法會傳入一個參數表示需要刪除的Worker對象;在進行Worker回收的時候會調用tryTerminate方法來嘗試關閉線程池,在tryTerminate方法裡面會檢查是否有Worker在工作,檢查線程池的狀態,沒問題的話就會將當前線程池的狀態過渡到Tidying,之後調用terminated方法,將線程池狀態更新到Terminated;
從上面的分析中,我們可以看出線程池運行的4個階段:
(1):poolSize corePoolSize,則直接創建新的線程(核心線程)來執行當前提交的任務;
(2):poolSize = corePoolSize,並且此時阻塞隊列沒有滿,那麼會將當前任務添加到阻塞隊列中,如果此時存在工作線程(非核心線程)的話,那麼會由工作線程來處理該阻塞隊列中的任務,如果此時工作線程數量為0的話,那麼會創建一個工作線程(非核心線程)出來;
(3):poolSize = corePoolSize,並且此時阻塞隊列已經滿了,那麼會直接創建新的工作線程(非核心線程)來處理阻塞隊列中的任務;
(4):poolSize = maximumPoolSize,並且此時阻塞隊列也滿了的話,那麼會觸發拒絕機制,具體決絕策略採用的是什麼就要看我們創建ThreadPoolExecutor的時候傳入的RejectExecutionHandler參數了;
接下來就是線程池是怎麼做到重用線程的呢?
個人認為線程池裡面重用線程的工作是在getTask裡面實現的,在getTask裡面是存在兩個for死循環嵌套的,他會不斷的從阻塞對列裡面取出需要執行的任務,返回給我們的runWorker方法裡面,而在runWorker方法裡面只要getTask返回的任務不是空就會執行該任務的run方法來處理它,這樣一直執行下去,直到getTask返回空為止,此時的情況就是阻塞隊列裡面沒有任務了,這樣一個線程處理完一個任務之後接著再處理阻塞隊列中的另一個任務,當然在線程池中的不同線程是可以並發處理阻塞隊列中的任務的,最後在阻塞隊列內部不存在任務的時候會去判斷是否需要回收Worker對象,其實Worker對象的個數就是線程池中線程的個數,至於什麼情況才需要回收,上面已經說了,就是四種情況了;
最後就是線程池是怎樣被關閉的呢?
涉及到線程池的關閉,需要用到兩個方法,shutdown和shutdownNow,他們都是位於ThreadPoolExecutor裡面的,對於shutdown的話,他會將線程池狀態切換成Shutdown,此時是不會影響對阻塞隊列中任務執行的,但是會拒絕執行新加進來的任務,同時會回收閑置的Worker;而shutdownNow方法會將線程池狀態切換成Stop,此時既不會再去處理阻塞隊列裡面的任務,也不會去處理新加進來的任務,同時會回收所有Worker;
面試題:線程池有幾種實現方式,線程池的七大參數有哪些?
在Java編碼的過程中,我們經常會創建一個線程來提高程序的執行效率,雖然這樣實現起來很方便,但是會有一個問題:如果並發的線程數多,並且每個線程都是執行一個時間很短的任務就結束了,這樣會造成頻繁的創建和銷毀線程從而導致降低系統的效率。
那麼問題來了,有沒有辦法可用復用創建好的線程呢,也就是線程執行完一個任務後,不被銷毀,繼續執行其他的任務?
在Java可以通過線程池來實現這樣的效果。
下面從三個方面和大家一起來探討一下Java線程池相關的內容。
1.Java中的ThreadPoolExecutor類。
2.Java中4種線程池的使用。
3.Java線程池常用參數如何設置。
一、Java中的ThreadPoolExecutor類
A.ThreadPoolExecutor的重要參數
1.corePoolSize:核心線程數
核心線程會一直存活,及時沒有任務需要執行。
當線程數小於核心線程數時,即使有線程空閑,線程池也會優先創建新線程處理。
設置allowCoreThreadTimeout=true(默認false)時,核心線程會超時關閉。
2.queueCapacity:任務隊列容量(阻塞隊列)
當核心線程數達到最大時,新任務會放在隊列中排隊等待執行。
3.maxPoolSize:最大線程數
當線程數=corePoolSize,且任務隊列已滿時。線程池會創建新線程來處理任務。
當線程數=maxPoolSize,且任務隊列已滿時,線程池會拒絕處理任務而拋出異常。
4.keepAliveTime:線程空閑時間
當線程空閑時間達到keepAliveTime時,線程會退出,直到線程數量=corePoolSize。
如果allowCoreThreadTimeout=true,則會直到線程數量=0。
5.allowCoreThreadTimeout:允許核心線程超時
6.rejectedExecutionHandler:任務拒絕處理器。
B.ThreadPoolExecutor執行過程
1.當線程數小於核心線程數時,創建線程。
2.當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。
3.當線程數大於等於核心線程數,且任務隊列已滿。(1)若線程數小於最大線程數,創建線程。(2)若線程數等於最大線程數,拋出異常,拒絕任務。
二、Java中4種線程池
Java通過Executors提供四種線程池,分別為:
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。
newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/189122.html