以前,我總覺得的買一件東西,做一件事,或者從某一個時間節點開始,我的生命就會發生轉折,一切就會無比順利,立馬變厲害。但是,事實上並不是如此。我不可能馬上變厲害,也不可能一口吃成一個胖子。看一篇文章也不能讓你從此走上人生巔峰,越來越相信,這是一個長期的過程,只有量變引起質變,縱使緩慢,馳而不息。
如何設計一個線程池?
三個步驟
這是一個常見的問題,如果在比較熟悉線程池運作原理的情況下,這個問題並不難。設計實現一個東西,三步走:是什么?為什么?怎么做?
線程池是什么?
線程池使用了池化技術,將線程存儲起來放在一個 "池子"(容器)里面,來了任務可以用已有的空閑的線程進行處理, 處理完成之后,歸還到容器,可以復用。如果線程不夠,還可以根據規則動態增加,線程多余的時候,亦可以讓多余的線程死亡。
為什么要用線程池?
實現線程池有什么好處呢?
- 降低資源消耗:池化技術可以重復利用已經創建的線程,降低線程創建和銷毀的損耗。
- 提高響應速度:利用已經存在的線程進行處理,少去了創建線程的時間
- 管理線程可控:線程是稀缺資源,不能無限創建,線程池可以做到統一分配和監控
- 拓展其他功能:比如定時線程池,可以定時執行任務
需要考慮的點
那線程池設計需要考慮的點:
-
線程池狀態:
- 有哪些狀態?如何維護狀態?
-
線程
- 線程怎么封裝?線程放在哪個池子里?
- 線程怎么取得任務?
- 線程有哪些狀態?
- 線程的數量怎么限制?動態變化?自動伸縮?
- 線程怎么消亡?如何重復利用?
-
任務
- 任務少可以直接處理,多的時候,放在哪里?
- 任務隊列滿了,怎么辦?
- 用什么隊列?
如果從任務的階段來看,分為以下幾個階段:
- 如何存任務?
- 如何取任務?
- 如何執行任務?
- 如何拒絕任務?
線程池狀態
狀態有哪些?如何維護狀態?
狀態可以設置為以下幾種:
- RUNNING:運行狀態,可以接受任務,也可以處理任務
- SHUTDOWN:不可以接受任務,但是可以處理任務
- STOP:不可以接受任務,也不可以處理任務,中斷當前任務
- TIDYING:所有線程停止
- TERMINATED:線程池的最后狀態
各種狀態之間是不一樣的,他們的狀態之間變化如下:
而維護狀態的話,可以用一個變量單獨存儲,並且需要保證修改時的原子性,在底層操作系統中,對int的修改是原子的,而在32位的操作系統里面,對double
,long
這種64位數值的操作不是原子的。除此之外,實際上JDK里面實現的狀態和線程池的線程數是同一個變量,高3位表示線程池的狀態,而低29位則表示線程的數量。
這樣設計的好處是節省空間,並且同時更新的時候有優勢。
線程相關
線程怎么封裝?線程放在哪個池子里?
線程,即是實現了Runnable
接口,執行的時候,調用的是start()
方法,但是start()
方法內部編譯后調用的是 run()
方法,這個方法只能調用一次,調用多次會報錯。因此線程池里面的線程跑起來之后,不可能終止再啟動,只能一直運行着。既然不可以停止,那么執行完任務之后,沒有任務過來,只能是輪詢取出任務的過程
線程可以運行任務,因此封裝線程的時候,假設封裝成為 Worker
, Worker
里面必定是包含一個 Thread
,表示當前線程,除了當前線程之外,封裝的線程類還應該持有任務,初始化可能直接給予任務,當前的任務是null的時候才需要去獲取任務。
可以考慮使用 HashSet
來存儲線程,也就是充當線程池的角色,當然,HashSet
會有線程安全的問題需要考慮,那么我們可以考慮使用一個可重入鎖比如 ReentrantLock
,凡是增刪線程池的線程,都需要鎖住。
private final ReentrantLock mainLock = new ReentrantLock();
線程怎么取得任務?
(1)初始化線程的時候可以直接指定任務,譬如Runnable firstTask
,將任務封裝到 worker
中,然后獲取 worker
里面的 thread
,thread.run()
的時候,其實就是 跑的是 worker
本身的 run()
方法,因為 worker
本身就是實現了 Runnable
接口,里面的線程其實就是其本身。因此也可以實現對 ThreadFactory
線程工廠的定制化。
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
final Thread thread;
Runnable firstTask;
...
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 從線程池創建線程,傳入的是其本身
this.thread = getThreadFactory().newThread(this);
}
}
(2)運行完任務的線程,應該繼續取任務,取任務肯定需要從任務隊列里面取,要是任務隊列里面沒有任務,由於是阻塞隊列,那么可以等待,如果等待若干時間后,仍沒有任務,倘若該線程池的線程數已經超過核心線程數,並且允許線程消亡的話,應該將該線程從線程池中移除,並結束掉該線程。
取任務和執行任務,對於線程池里面的線程而言,就是一個周而復始的工作,除非它會消亡。
線程有哪些狀態?
現在我們所說的是Java
中的線程Thread
,一個線程在一個給定的時間點,只能處於一種狀態,這些狀態都是虛擬機的狀態,不能反映任何操作系統的線程狀態,一共有六種/七種狀態:
-
NEW
:創建了線程對象,但是還沒有調用Start()
方法,還沒有啟動的線程處於這種狀態。 -
Running
:運行狀態,其實包含了兩種狀態,但是Java
線程將就緒和運行中統稱為可運行Runnable
:就緒狀態:創建對象后,調用了start()
方法,該狀態的線程還位於可運行線程池中,等待調度,獲取CPU
的使用權- 只是有資格執行,不一定會執行
start()
之后進入就緒狀態,sleep()
結束或者join()
結束,線程獲得對象鎖等都會進入該狀態。CPU
時間片結束或者主動調用yield()
方法,也會進入該狀態
Running
:獲取到CPU
的使用權(獲得CPU時間片),變成運行中
-
BLOCKED
:阻塞,線程阻塞於鎖,等待監視器鎖,一般是Synchronize
關鍵字修飾的方法或者代碼塊 -
WAITING
:進入該狀態,需要等待其他線程通知(notify
)或者中斷,一個線程無限期地等待另一個線程。 -
TIMED_WAITING
:超時等待,在指定時間后自動喚醒,返回,不會一直等待 -
TERMINATED
:線程執行完畢,已經退出。如果已終止再調用start(),將會拋出java.lang.IllegalThreadStateException
異常。
線程的數量怎么限制?動態變化?自動伸縮?
線程池本身,就是為了限制和充分使用線程資的,因此有了兩個概念:核心線程數,最大線程數。
要想讓線程數根據任務數量動態變化,那么我們可以考慮以下設計(假設不斷有任務):
- 來一個任務創建一個線程處理,直到線程數達到核心線程數。
- 達到核心線程數之后且沒有空閑線程,來了任務直接放到任務隊列。
- 任務隊列如果是無界的,會被撐爆。
- 任務隊列如果是有界的,任務隊列滿了之后,還有任務過來,會繼續創建線程處理,此時線程數大於核心線程數,直到線程數等於最大線程數。
- 達到最大線程數之后,還有任務不斷過來,會觸發拒絕策略,根據不同策略進行處理。
- 如果任務不斷處理完成,任務隊列空了,線程空閑沒任務,會在一定時間內,銷毀,讓線程數保持在核心線程數即可。
由上面可以看出,主要控制伸縮的參數是核心線程數
,最大線程數
,任務隊列
,拒絕策略
。
線程怎么消亡?如何重復利用?
線程不能被重新調用多次start()
,因此只能調用一次,也就是線程不可能停下來,再啟動。那么就說明線程復用只是在不斷的循環罷了。
消亡只是結束了它的run()
方法,當線程池數量需要自動縮容的,就會讓一部分空閑的線程結束。
而重復利用,其實是執行完任務之后,再去去任務隊列取任務,取不到任務會等待,任務隊列是一個阻塞隊列,這是一個不斷循環
的過程。
任務相關
任務少可以直接處理,多的時候,放在哪里?
任務少的時候,來了直接創建,賦予線程初始化任務,就可開始執行,任務多的時候,把它放進隊列里面,先進先出。
任務隊列滿了,怎么辦?
任務隊列滿了,會繼續增加線程,直到達到最大的線程數。
用什么隊列?
一般的隊列,只是一個有限長度的緩沖區,要是滿了,就不能保存當前的任務,阻塞隊列可以通過阻塞,保留出當前需要入隊的任務,只是會阻塞等待。同樣的,阻塞隊列也可以保證任務隊列沒有任務的時候,阻塞當前獲取任務的線程,讓它進入wait
狀態,釋放cpu
的資源。因此在線程池的場景下,阻塞隊列其實是比較有必要的。
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。這個世界希望一切都很快,更快,但是我希望自己能走好每一步,寫好每一篇文章,期待和你們一起交流。如果有幫助,順手點個贊,對我,是莫大的鼓勵和認可。