更新時間:2019-08-30 14:28:18 來源:動力節(jié)點 瀏覽2408次
我們在工作中或多或少都使用過線程池,但是為什么要使用線程池呢?從他的名字中我們就應(yīng)該知道,線程池使用了一種池化技術(shù),和很多其他池化技術(shù)一樣,都是為了更高效的利用資源,例如鏈接池,內(nèi)存池等等。
數(shù)據(jù)庫鏈接是一種很昂貴的資源,創(chuàng)建和銷毀都需要付出高昂的代價,為了避免頻繁的創(chuàng)建數(shù)據(jù)庫鏈接,所以產(chǎn)生了鏈接池技術(shù)。優(yōu)先在池子中創(chuàng)建一批數(shù)據(jù)庫鏈接,有需要訪問數(shù)據(jù)庫時,直接到池子中去獲取一個可用的鏈接,使用完了之后再歸還到鏈接池中去。
同樣的,線程也是一種寶貴的資源,并且也是一種有限的資源,創(chuàng)建和銷毀線程也同樣需要付出不菲的代價。我們所有的代碼都是由一個一個的線程支撐起來的,如今的芯片架構(gòu)也決定了我們必須編寫多線程執(zhí)行的程序,以獲取最高的程序性能。
那么怎樣高效的管理多線程之間的分工與協(xié)作就成了一個關(guān)鍵問題,DougLea大神為我們設(shè)計并實現(xiàn)了一款線程池工具,通過該工具就可以實現(xiàn)多線程的能力,并實現(xiàn)任務(wù)的高效執(zhí)行與調(diào)度。
為了正確合理的使用線程池工具,我們有必要對線程池的原理進(jìn)行了解。
本篇文章主要從三個方面來對線程池進(jìn)行分析:線程池狀態(tài)、重要屬性、工作流程。
線程池狀態(tài)
首先線程池是有狀態(tài)的,這些狀態(tài)標(biāo)識這線程池內(nèi)部的一些運行情況,線程池的開啟到關(guān)閉的過程就是線程池狀態(tài)的一個流轉(zhuǎn)的過程。
線程池共有五種狀態(tài):
重要屬性
一個線程池的核心參數(shù)有很多,每個參數(shù)都有著特殊的作用,各個參數(shù)聚合在一起后將完成整個線程池的完整工作。
1、線程狀態(tài)和工作線程數(shù)量
首先線程池是有狀態(tài)的,不同狀態(tài)下線程池的行為是不一樣的,5種狀態(tài)已經(jīng)在上面說過了。
另外線程池肯定是需要線程去執(zhí)行具體的任務(wù)的,所以在線程池中就封裝了一個內(nèi)部類Worker作為工作線程,每個Worker中都維持著一個Thread。
線程池的重點之一就是控制線程資源合理高效的使用,所以必須控制工作線程的個數(shù),所以需要保存當(dāng)前線程池中工作線程的個數(shù)。
看到這里,你是否覺得需要用兩個變量來保存線程池的狀態(tài)和線程池中工作線程的個數(shù)呢?但是在ThreadPoolExecutor中只用了一個AtomicInteger型的變量就保存了這兩個屬性的值,那就是ctl。
ctl的高3位用來表示線程池的狀態(tài)(runState),低29位用來表示工作線程的個數(shù)(workerCnt),為什么要用3位來表示線程池的狀態(tài)呢,原因是線程池一共有5種狀態(tài),而2位只能表示出4種情況,所以至少需要3位才能表示得了5種狀態(tài)。
2、核心線程數(shù)和最大線程數(shù)
現(xiàn)在有了標(biāo)志工作線程的個數(shù)的變量了,那到底該有多少個線程才合適呢?線程多了浪費線程資源,少了又不能發(fā)揮線程池的性能。
為了解決這個問題,線程池設(shè)計了兩個變量來協(xié)作,分別是:
核心線程數(shù):corePoolSize用來表示線程池中的核心線程的數(shù)量,也可以稱為可閑置的線程數(shù)量最大線程數(shù):maximumPoolSize用來表示線程池中最多能夠創(chuàng)建的線程數(shù)量
現(xiàn)在我們有一個疑問,既然已經(jīng)有了標(biāo)識工作線程的個數(shù)的變量了,為什么還要有核心線程數(shù)、最大線程數(shù)呢?
其實你這樣想就能夠理解了,創(chuàng)建線程是有代價的,不能每次要執(zhí)行一個任務(wù)時就創(chuàng)建一個線程,但是也不能在任務(wù)非常多的時候,只有少量的線程在執(zhí)行,這樣任務(wù)是來不及處理的,而是應(yīng)該創(chuàng)建合適的足夠多的線程來及時的處理任務(wù)。隨著任務(wù)數(shù)量的變化,當(dāng)任務(wù)數(shù)明顯很小時,原本創(chuàng)建的多余的線程就沒有必要再存活著了,因為這時使用少量的線程就能夠處理的過來了,所以說真正工作的線程的數(shù)量,是隨著任務(wù)的變化而變化的。
那核心線程數(shù)和最大線程數(shù)與工作線程個數(shù)的關(guān)系是什么呢?
工作線程的個數(shù)可能從0到最大線程數(shù)之間變化,當(dāng)執(zhí)行一段時間之后可能維持在corePoolSize,但也不是絕對的,取決于核心線程是否允許被超時回收。
3、創(chuàng)建線程的工廠
既然是線程池,那自然少不了線程,線程該如何來創(chuàng)建呢?這個任務(wù)就交給了線程工廠ThreadFactory來完成。
4、緩存任務(wù)的阻塞隊列
上面我們說了核心線程數(shù)和最大線程數(shù),并且也介紹了工作線程的個數(shù)是在0和最大線程數(shù)之間變化的。但是不可能一下子就創(chuàng)建了所有線程,把線程池裝滿,而是有一個過程,這個過程是這樣的:
當(dāng)線程池接收到一個任務(wù)時,如果工作線程數(shù)沒有達(dá)到corePoolSize,那么就會新建一個線程,并綁定該任務(wù),直到工作線程的數(shù)量達(dá)到corePoolSize前都不會重用之前的線程。
當(dāng)工作線程數(shù)達(dá)到corePoolSize了,這時又接收到新任務(wù)時,會將任務(wù)存放在一個阻塞隊列中等待核心線程去執(zhí)行。為什么不直接創(chuàng)建更多的線程來執(zhí)行新任務(wù)呢,原因是核心線程中很可能已經(jīng)有線程執(zhí)行完自己的任務(wù)了,或者有其他線程馬上就能處理完當(dāng)前的任務(wù),并且接下來就能投入到新的任務(wù)中去,所以阻塞隊列是一種緩沖的機(jī)制,給核心線程一個機(jī)會讓他們充分發(fā)揮自己的能力。另外一個值得考慮的原因是,創(chuàng)建線程畢竟是比較昂貴的,不可能一有任務(wù)要執(zhí)行就去創(chuàng)建一個新的線程。
所以我們需要為線程池配備一個阻塞隊列,用來臨時緩存任務(wù),這些任務(wù)將等待工作線程來執(zhí)行。
5、非核心線程存活時間
上面我們說了當(dāng)工作線程數(shù)達(dá)到corePoolSize時,線程池會將新接收到的任務(wù)存放在阻塞隊列中,而阻塞隊列又兩種情況:一種是有界的隊列,一種是無界的隊列。
如果是無界隊列,那么當(dāng)核心線程都在忙的時候,所有新提交的任務(wù)都會被存放在該無界隊列中,這時最大線程數(shù)將變得沒有意義,因為阻塞隊列不會存在被裝滿的情況。
如果是有界隊列,那么當(dāng)阻塞隊列中裝滿了等待執(zhí)行的任務(wù),這時再有新任務(wù)提交時,線程池就需要創(chuàng)建新的“臨時”線程來處理,相當(dāng)于增派人手來處理任務(wù)。
但是創(chuàng)建的“臨時”線程是有存活時間的,不可能讓他們一直都存活著,當(dāng)阻塞隊列中的任務(wù)被執(zhí)行完畢,并且又沒有那么多新任務(wù)被提交時,“臨時”線程就需要被回收銷毀,在被回收銷毀之前等待的這段時間,就是非核心線程的存活時間,也就是keepAliveTime屬性。
那么什么是“非核心線程”呢?是不是先創(chuàng)建的線程就是核心線程,后創(chuàng)建的就是非核心線程呢?
其實核心線程跟創(chuàng)建的先后沒有關(guān)系,而是跟工作線程的個數(shù)有關(guān),如果當(dāng)前工作線程的個數(shù)大于核心線程數(shù),那么所有的線程都可能是“非核心線程”,都有被回收的可能。
一個線程執(zhí)行完了一個任務(wù)后,會去阻塞隊列里面取新的任務(wù),在取到任務(wù)之前它就是一個閑置的線程。
取任務(wù)的方法有兩種,一種是通過take()方法一直阻塞直到取出任務(wù),另一種是通過poll(keepAliveTime,timeUnit)方法在一定時間內(nèi)取出任務(wù)或者超時,如果超時這個線程就會被回收,請注意核心線程一般不會被回收。
那么怎么保證核心線程不會被回收呢?還是跟工作線程的個數(shù)有關(guān),每一個線程在取任務(wù)的時候,線程池會比較當(dāng)前的工作線程個數(shù)與核心線程數(shù):
如果工作線程數(shù)小于當(dāng)前的核心線程數(shù),則使用第一種方法取任務(wù),也就是沒有超時回收,這時所有的工作線程都是“核心線程”,他們不會被回收;如果大于核心線程數(shù),則使用第二種方法取任務(wù),一旦超時就回收,所以并沒有絕對的核心線程,只要這個線程沒有在存活時間內(nèi)取到任務(wù)去執(zhí)行就會被回收。
所以每個線程想要保住自己“核心線程”的身份,必須充分努力,盡可能快的獲取到任務(wù)去執(zhí)行,這樣才能逃避被回收的命運。
核心線程一般不會被回收,但是也不是絕對的,如果我們設(shè)置了允許核心線程超時被回收的話,那么就沒有核心線程這種說法了,所有的線程都會通過poll(keepAliveTime,timeUnit)來獲取任務(wù),一旦超時獲取不到任務(wù),就會被回收,一般很少會這樣來使用,除非該線程池需要處理的任務(wù)非常少,并且頻率也不高,不需要將核心線程一直維持著。
6、拒絕策略
雖然我們有了阻塞隊列來對任務(wù)進(jìn)行緩存,這從一定程度上為線程池的執(zhí)行提供了緩沖期,但是如果是有界的阻塞隊列,那就存在隊列滿的情況,也存在工作線程的數(shù)據(jù)已經(jīng)達(dá)到最大線程數(shù)的時候。如果這時候再有新的任務(wù)提交時,顯然線程池已經(jīng)心有余而力不足了,因為既沒有空余的隊列空間來存放該任務(wù),也無法創(chuàng)建新的線程來執(zhí)行該任務(wù)了,所以這時我們就需要有一種拒絕策略,即handler。
拒絕策略是一個RejectedExecutionHandler類型的變量,用戶可以自行指定拒絕的策略,如果不指定的話,線程池將使用默認(rèn)的拒絕策略:拋出異常。
在線程池中還為我們提供了很多其他可以選擇的拒絕策略:
直接丟棄該任務(wù)使用調(diào)用者線程執(zhí)行該任務(wù)丟棄任務(wù)隊列中的最老的一個任務(wù),然后提交該任務(wù)
工作流程
了解了線程池中所有的重要屬性之后,現(xiàn)在我們需要來了解下線程池的工作流程了。
上圖是一張線程池工作的精簡圖,實際的過程比這個要復(fù)雜的多,不過這些應(yīng)該能夠完全覆蓋到線程池的整個工作流程了。
整個過程可以拆分成以下幾個部分:
1、提交任務(wù)
當(dāng)向線程池提交一個新的任務(wù)時,線程池有三種處理情況,分別是:創(chuàng)建一個工作線程來執(zhí)行該任務(wù)、將任務(wù)加入阻塞隊列、拒絕該任務(wù)。
提交任務(wù)的過程也可以拆分成以下幾個部分:
當(dāng)工作線程數(shù)小于核心線程數(shù)時,直接創(chuàng)建新的核心工作線程當(dāng)工作線程數(shù)不小于核心線程數(shù)時,就需要嘗試將任務(wù)添加到阻塞隊列中去如果能夠加入成功,說明隊列還沒有滿,那么需要做以下的二次驗證來保證添加進(jìn)去的任務(wù)能夠成功被執(zhí)行驗證當(dāng)前線程池的運行狀態(tài),如果是非RUNNING狀態(tài),則需要將任務(wù)從阻塞隊列中移除,然后拒絕該任務(wù)驗證當(dāng)前線程池中的工作線程的個數(shù),如果為0,則需要主動添加一個空工作線程來執(zhí)行剛剛添加到阻塞隊列中的任務(wù)如果加入失敗,則說明隊列已經(jīng)滿了,那么這時就需要創(chuàng)建新的“臨時”工作線程來執(zhí)行任務(wù)如果創(chuàng)建成功,則直接執(zhí)行該任務(wù)如果創(chuàng)建失敗,則說明工作線程數(shù)已經(jīng)等于最大線程數(shù)了,則只能拒絕該任務(wù)了
整個過程可以用下面這張圖來表示:
2、創(chuàng)建工作線程
創(chuàng)建工作線程需要做一系列的判斷,需要確保當(dāng)前線程池可以創(chuàng)建新的線程之后,才能創(chuàng)建。
首先,當(dāng)線程池的狀態(tài)是SHUTDOWN或者STOP時,則不能創(chuàng)建新的線程。
另外,當(dāng)線程工廠創(chuàng)建線程失敗時,也不能創(chuàng)建新的線程。
還有就是當(dāng)前工作線程的數(shù)量與核心線程數(shù)、最大線程數(shù)進(jìn)行比較,如果前者大于后者的話,也不允許創(chuàng)建。
除此之外,會嘗試通過CAS來自增工作線程的個數(shù),如果自增成功了,則會創(chuàng)建新的工作線程,即Worker對象。
然后加鎖進(jìn)行二次驗證是否能夠創(chuàng)建工作線程,最后如果創(chuàng)建成功,則會啟動該工作線程。
3、啟動工作線程
當(dāng)工作線程創(chuàng)建成功后,也就是Worker對象已經(jīng)創(chuàng)建好了,這時就需要啟動該工作線程,讓線程開始干活了,Worker對象中關(guān)聯(lián)著一個Thread,所以要啟動工作線程的話,只要通過worker.thread.start()來啟動該線程即可。
啟動完了之后,就會執(zhí)行Worker對象的run方法,因為Worker實現(xiàn)了Runnable接口,所以本質(zhì)上Worker也是一個線程。
通過線程start開啟之后就會調(diào)用到Runnable的run方法,在worker對象的run方法中,調(diào)用了runWorker(this)方法,也就是把當(dāng)前對象傳遞給了runWorker方法,讓他來執(zhí)行。
4、獲取任務(wù)并執(zhí)行
在runWorker方法被調(diào)用之后,就是執(zhí)行具體的任務(wù)了,首先需要拿到一個可以執(zhí)行的任務(wù),而Worker對象中默認(rèn)綁定了一個任務(wù),如果該任務(wù)不為空的話,那么就是直接執(zhí)行。
執(zhí)行完了之后,就會去阻塞隊列中獲取任務(wù)來執(zhí)行,而獲取任務(wù)的過程,需要考慮當(dāng)前工作線程的個數(shù)。
如果工作線程數(shù)大于核心線程數(shù),那么就需要通過poll來獲取,因為這時需要對閑置的線程進(jìn)行回收;如果工作線程數(shù)小于等于核心線程數(shù),那么就可以通過take來獲取了,因此這時所有的線程都是核心線程,不需要進(jìn)行回收,前提是沒有設(shè)置allowCoreThreadTimeOut
以上就是動力節(jié)點java培訓(xùn)機(jī)構(gòu)小編分享的“Java視頻教程:Java線程池的工作原理”的內(nèi)容,希望能夠幫助到各位小伙伴們,更多java最新資訊請關(guān)注動力節(jié)點java培訓(xùn)機(jī)構(gòu)官網(wǎng),每天會有精彩內(nèi)容分享與你。
相關(guān)閱讀
初級 202925
初級 203221
初級 202629
初級 203743