我們在工作中或多或少都使用過線程池。但是為什么要使用線程池呢?從它的名稱中我們就可以猜到,線程池是使用了一種池化技術(Pooling Technology)。和很多其他池化技術一樣,都是為了更高效的利用資源,例如連接池,內存池等。
數據庫連接是一種很昂貴的資源,創建和銷毀都需要付出高昂的代價。為了避免頻繁地創建數據庫連接,所以產生了數據庫連接池技術。優先在池子中創建一批數據庫連接,當有需要訪問數據庫時,直接到池子中去獲取一個可用的連接,使用完了之后再歸還到連接池中去。
同樣的,線程也是一種很寶貴的資源,並且也是一種有限的資源,創建和銷毀線程也同樣需要付出不菲的代價。我們所有的代碼執行都是由一個一個的線程支撐起來的,如今的芯片架構也決定了我們必須編寫多線程執行的程序,以獲得最高的程序性能。那么怎樣高效地管理多線程之間的分工與協作就成了一個關鍵問題,Doug Lea大神為我們設計並實現了一款線程池工具,通過該工具就可以實現多線程的能力,並實現任務的高效執行與調度。為了正確合理地使用線程池工具,我們有必要對線程池的原理進行了解。
了解線程池工作原理主要有三個方面:線程池狀態、線程池的重要屬性和線程池的工作流程。
線程池狀態
線程池是有狀態的,這些狀態標識這個線程池內部的一些運行情況。線程池的開啟到關閉的過程就是線程池狀態的一個流轉過程。
線程池共有5種狀態:
運行狀態(RUNNING):此狀態下,線程池可以接受新的任務,也可以處理阻塞隊列中的任務。執行shutdown()方法可進入待關閉(SHUTDOWN)狀態,執行shutdownNow()方法可進入停止(STOP)狀態。
待關閉狀態(SHUTDOWN):此狀態下,線程池不再接受新的任務,繼續處理阻塞隊列中的任務。當阻塞隊列中的任務為空,且工作線程數為0的時候,進入整理(TIDYING)狀態。
停止狀態(STOP):此狀態下,線程池不接受新任務,也不處理阻塞隊列中的任務,反而會嘗試結束執行中的任務。當工作線程數為0時,進入整理(TIDYING)狀態。
整理狀態(TIDYING):此狀態下,所有任務都已經執行完畢,且沒有工作線程。執行terminated()方法進入終止(TERMINATED)狀態。
終止狀態(TERMINATED):此狀態下,線程池完全終止,並完成了所有資源的釋放。
線程池的重要屬性
一個線程池的核心參數有很多,每個參數都有着特殊的作用,各個參數聚合再一起后將完成整個線程池的完整工作。其中的六個尤為重要:線程狀態和工作線程的數量,核心線程數和最大線程數,創建線程的工廠,緩存任務的阻塞隊列,非核心線程存活的時間和拒絕策略。
線程狀態和工作線程數量
首先線程池是有狀態的,在不同的狀態下,線程池的行為是不一樣的。
然后線程池肯定是需要線程去執行具體的任務,所以在線程池中就封裝了一個內部類Worker作為工作線程,每個Worker中都維持着一個Thread。
線程池的重點之一,就是控制線程資源合理高效的使用,所以必須控制工作線程的個數,所以需要保存當前線程池中工作線程的個數。
看到這里,你是否覺得需要用兩個變量來保存線程池的狀態和線程池中工作線程的個數呢?但是在ThreadPoolExecutor中只用了一個AtomicInteger型的變量就保存了這兩個屬性的值,那就是ctl。
ctl是一個原子操作類型(AtomicInteger)的變量。ctl的高3位用來表示線程池的狀態(runState),低29位用來表示工作線程的個數(workerCnt)。為什么要用3位來表示線程池的狀態呢,原因是因為線程池一共有5種狀態,而2位只能表示出4種情況(2位是2^2,最多產生4種結果),至少需要3位才能表示得了全部的5種狀態(3位是3^2,最多產生9種結果)。
核心線程數和最大線程數
現在有了標識工作線程的個數的變量了,那到底該有多少個線程才合適呢?線程多了會浪費線程資源,少了又不能發揮線程池的性能。
為了解決這個問題,線程池設計了兩個變量來協作,分別是:
核心線程數(corePoolSize):用來表示線程池中的核心線程的數量,也可以稱為可閑置的線程數量。
最大線程數(maximumPoolSize):用來表示線程池中最多能夠創建的線程數量。
現在我們有一個疑惑,既然已經有了標識工作線程的個數的變量了,為什么還要有核心線程數和最大線程數呢?
其實你這樣想就能夠理解了,創建線程是有代價的,不能每次要執行一個任務時就創建一個線程,但是也不能在任務非常多的時候,只有少量的線程在執行,這樣任務是來不及處理的,而是應該創建合適的足夠多的線程來及時地處理任務。
隨着任務數量的變化,當任務數量明顯減少時,原本創建的多余的線程就沒有必要再存活着了,因為這時使用少量的線程就能夠處理得過來了,所以說真正工作的線程的數量,是隨着任務的變化而變化的。
那核心線程數和最大線程數和工作線程個數的關系是什么呢?
工作線程的個數可能從0到最大線程數之間變化,當執行一段時間之后可能維持在核心線程數(corePoolSize),但也不是絕對的,取決於核心線程是否允許被超時回收。
創建線程的工廠
既然是線程池,那自然少不了線程。線程該如何來創建呢?這個任務就交給了線程工廠ThreadFactory來完成。
緩存任務的阻塞隊列
上面我們說了核心線程數和最大線程數,並且也介紹了工作線程的個數是在0和最大線程數之間變化的。但是不可能一下子就創建了所有線程,把線程池裝滿,而是有一個過程:
當線程池接受到一個任務時,如果工作線程數沒有達到corePoolSize,那么就會新建一個線程,並綁定該任務,知道工作線程的數量達到corePoolSize前都不會重用之前創建的線程。
當工作線程數達到corePoolSize了,這是又接收到新任務時,會將任務存放在一個阻塞隊列(workQueue)中等待核心線程去執行。為什么不直接創建更多的線程來執行新任務呢?原因是核心線程中很可能已經有線程執行完自己的任務了,或者有其他線程馬上就能處理完當前的任務,並且接下來就能投入到新的任務中去,所以阻塞隊列是一種緩沖機制,給核心線程一個機會讓他們充分發揮自己的能力。另外一個值得考慮的原因是,創建線程畢竟是代價昂貴的,不可能一有任務要執行就去創建一個新的線程。
所以我們需要為線程池配備一個阻塞隊列,用來臨時緩存任務,這些任務將等待工作線程來執行。
非核心線程存活時間
上面我們說了,當工作線程數達到corePoolSize時,線程池會將新接收到的任務放在阻塞隊列中,而阻塞隊列又分為兩種情況:一種是有界的隊列,一種是無界的隊列。
如果是無界隊列,那么當核心線程都在忙時,所有新提交的任務都會被存放在該無界隊列中,這時最大線程數將變得沒有意義,因為阻塞隊列不會存在被裝滿的情況。
如果是有界隊列,那么當阻塞隊列中裝滿了等待執行的任務,這時再有新任務提交時,線程池就需要創建新的臨時線程來處理,相當於增派人手來處理任務。
但是創建的臨時線程是有存活時間的,不可能讓它們一直都存活着,當阻塞隊列中的任務被執行完畢,並且又沒有那么多新任務被提交時,臨時線程就需要被回收銷毀,而在被回收銷毀之前等待的這段時間,就是非核心線程的存活時間,也就是keepAliveTime屬性。
那么什么是非核心線程呢?是不是先創建的線程就是核心線程,后創建的就是非核心線程呢?
其實核心線程跟創建的先后沒有關系,而是跟工作線程的個數有關,如果當前工作線程的個數大於核心線程數,那么所有的線程都可能是非核心線程,都有被回收的可能。
一個線程執行完一個任務后,會去阻塞隊列里面取新的任務,在取到任務之前,它就是一個閑置的線程。
取任務的方法有兩種,一種是通過take()方法一直阻塞直到取出任務,另一種是通過poll(keepAliveTime, timeUnit)方法在一定時間內取出任務或者超時,如果超時這個線程就會被回收,請注意核心線程一般不會被回收。
那么怎么保證核心線程不會被回收呢?還是跟工作線程的個數有關,每一個線程在取任務的時候,線程池會比較當前的工作線程個數與核心線程數。
1.如果工作線程數小於當前的核心線程數,則使用第一種方法取任務,也就是沒有超時回收,這時所有的工作線程都是核心線程,它們不會被回收。
2.如果工作線程數大於核心線程數,則使用第二種方法取任務,一旦超時就回收,所以並沒有絕對的核心線程,只要這個線程沒有在存活時間內取到任務去執行就會被回收。
所以每個線程如果想要保住自己核心線程的身份,必須充分努力,盡可能快得獲取到任務去執行,這樣才能避免被回收的命運。
核心線程一般不會被回收,但是也不是絕對的,如果我們設置了允許核心線程超時被回收的話,那么就沒有核心線程這種說法了,所有的線程都會通過poll(keepAliveTime, timeUnit)來獲取任務,一旦超時獲取不到任務,就會被回收,一般很少會這樣來使用,除非該線程池需要處理的任務非常少,並且頻率也不高,不需要將核心線程一直維持着。
拒絕策略
雖然我們有了阻塞隊列來對任務進行緩存,從一定程度上為線程池的執行提供了緩沖期,但是如果是有界的阻塞隊列,那就存在隊列滿的情況,也存在工作線程的數據已經達到最大線程數的時候。如果這時候再有新的任務提交時,顯然線程池已經心有余而力不足了,因為既沒有空余的隊列空間來存放該任務,也無法創建新的線程來執行該任務了,所以這時我們就需要有一種拒絕策略,即handler。
拒絕策略是一個RejectedExecutionHandler類型的變量,用戶可以自行指定拒絕的策略,如果不指定的話,線程池將使用默認的拒絕策略:拋出異常。
在線程池中還為我們提供了很多其他可以選擇的拒絕策略:
1.直接丟棄該任務
2.使用調用者線程執行該任務
3.丟棄任務隊列中的最老的一個任務,然后提交該任務
工作流程
了解了線程池中所有的重要屬性之后,現在我們需要來了解下線程池的工作流程了。
上面是一張線程池工作的精簡圖,實際的過程要比這個復雜得多,但是這些應該能夠完全覆蓋到線程池的整個工作流程了。
整個過程可以拆分成以下幾個部分:
提交任務
當向線程池提交一個新的任務時,線程池有三種處理情況,分別是:創建一個工作線程來執行該任務、將任務加入阻塞隊列、拒絕該任務。
提交任務的過程也可以拆分成以下幾個部分:
1.當工作線程數小於核心線程數時,直接創建新的核心工作線程。
2.當工作線程數大於核心線程數時,就需要嘗試將任務添加到阻塞隊列中去。
3.如果能夠加入成功,說明隊列還沒滿,那么就需要做以下的二次校驗來保證添加進去的任務能夠成功被執行。
4.驗證當前線程池中的運行狀態,如果是非RUNNING狀態,則需要將任務從阻塞隊列中移除,然后拒絕該任務。
5.驗證當前線程池中的工作線程的個數,如果是0,則需要主動添加一個空工作線程來執行剛剛添加到阻塞隊列中的任務。
6.如果加入失敗,說明隊列已經滿了,這時就需要創建新的臨時工作線程來執行任務。
7.如果創建成功,則直接執行該任務。
8.如果創建失敗,說明工作線程數已經等於最大線程數了,只能拒絕該任務了。
整個過程可以用下面這張圖來表示:
創建工作線程
創建工作線程需要做一系列的判斷,需要確保當前線程池可以創建新的線程之后,才能創建。
首先,當線程池的狀態是SHUTDOWN或者STOP時,不能創建新的線程。
其次,當線程工廠創建線程失敗時,也不能創建新的線程。
第三,拿當前工作線程的數量與核心線程數、最大線程數進行比較,如果前者大於后者的話,也不允許創建。
除此之外,線程池會嘗試通過CAS來自增工作線程的個數,如果自增成功了,則會創建新的工作線程,即Worker對象。
然后加鎖進行二次驗證是否能夠創建工作線程,如果最后創建成功,則會啟動該工作線程。
啟動工作線程
當工作線程創建成功后,也就是Worker對象已經創建好了,這時就需要啟動該工作線程,讓線程開始干活了,Worker對象中關聯着一個Thread,所以要啟動工作線程的話,只要通過worker.thread.start()來啟動該線程即可。
啟動完了之后,就會執行Worker對象的run方法,因為Worker實現了Runnable接口,所以本質上Worker也是一個線程。
通過線程start開啟之后就會調用到Runnable的run方法,在Worker對象的run方法中,調用了runWorker(this)方法,也就是把當前對象傳遞給了runWorker()方法,讓它來執行。
獲取任務並執行
在runWorker方法被調用之后,就是執行具體的任務了,首先需要拿到一個可以執行的任務,而Worker對象中默認綁定了一個任務,如果該任務不為空的話,那么就是直接執行。
執行完了之后,就會去阻塞隊列中獲取任務來執行。
獲取任務的過程則需要考慮當前工作線程的個數:
1.如果工作線程數大於核心線程數,那么就需要通過poll(keepAliveTime, timeUnit)來獲取,因為這時需要對閑置線程進行超時回收。
2.如果工作線程數小於等於核心線程數,那么就可以通過take()來獲取了。因為這時所有的線程都是核心線程,不需要進行回收,前提是沒有設置allowCoreThreadTimeOut(允許核心線程超時回收)為true。
"其實我原本話並不多的,只是一碰到你就多了。"