java的多執行緒
java的多執行緒的概念,向來都是很複雜、籠統、抽象的。現實世界只有將知識點抽象過後才能有效的傳播,但是傳播的過程中,只有將抽象的知識點具象化,我們才能習得。所以我們會將個別內容點進行一個具象化進而解剖。當我們理解完了之後最終將其抽象成一個個名詞:多執行緒、資源、鎖等。
本文僅從以下的範圍內容來談談java的多執行緒。
1. 何為執行緒,執行緒的作用
2. 資源的控制,鎖的介紹
3. 執行緒池的作用
4. 多執行緒的常用工具和方法
1.何為執行緒
官方解釋:執行緒是一個單一的順序控制流程。
執行緒分主執行緒、子執行緒。由主執行緒來建立子執行緒來執行各種任務。
舉例說明:以動漫“火影忍者”舉例說明,主執行緒就好比每一個忍者,他們構成了最基本的忍者世界,一個忍者可以按照任務的緩急、難易程度同時執行多個任務。同時忍者也能分身(呼叫自身查克拉)來分擔自身的任務,這就好比,忍者世界觀中的忍者主體,基本等同於程式中的主執行緒。主執行緒沒了,分身則主觀上不可控,就消失了。
由上圖可見,主執行緒與子執行緒的關係和忍者與分身的是很相似,也就是說,主執行緒能做的事,我們都能讓子執行緒幫我們做。忍者自己能做的事也能去靠分身去做。接下來,我們來看兩段程式碼。
程式碼一和程式碼二最終的結果都是隻做了一件事,向控制檯輸出“Hello World!”。但是程式碼一是由主執行緒去做的;程式碼二是由主執行緒建立的子執行緒去做的。這裡我們可以看出,主執行緒和子執行緒的本質上區分並不大,因為它們都執行相同的邏輯,這一點上並沒有進行區分。
程式如果都是按照單個執行緒的話,那麼所有任務的執行均是按照順序來進行(序列執行)。
而多執行緒的作用是可以安排不同的執行緒執行不同的任務。
上圖是一個理論值,我們在某些任務密集的場景下,多執行緒的執行效率多數情況下可能高於單執行緒的執行。
為什麼說是可能,因為這裡建立子執行緒是會消耗效能的,也帶有時間消耗,如果設定不合理,單單建立子執行緒的時間成本就遠大於執行任務的時間成本,這一點要結合實際場景進行考慮。
舉例說明:火影裡的忍者也不會接到一批任務就馬上分身去一個個的做,他們也得結合任務的實際情況來考慮使用分身。
1. 執行緒可以執行正確的邏輯程式碼
2. 執行緒的建立也伴隨著效能消耗,並不是無消耗
2.資源的控制
java中的資源可以理解為,一個例項或基礎資料型別的變數的任何操作。例項或變數在這裡不能完全算做資源,因為根據物件導向程式設計中的封裝性,程式碼中直接將例項暴露出來給非本類的例項進行操作是一個大忌。
這裡我們從以下資源和執行緒的關係進行解剖。
1. 一個執行緒可以執行多個資源(序列)
2. 多個執行緒可以執行多個資源(並行)
3. 單個資源可以被一個執行緒執行(串線)
4. 單個資源可以被多個執行緒執行(並行)
從這裡看,1和3沒什麼問題。因為這種機制下我們確保了一個資源被一個執行緒執行(等同於一個任務被一個忍者(本體或分身均可)執行)。但是2和4就出現了一個現象,同一個資源被多個執行緒所操作,如果不加以控制,則會出現指定之外的執行結果或者直接產生死鎖。
舉例說明:兩個忍者都執行了同一個任務,去殺死鄰國的頭目,我們假設忍者A過去殺死了頭目,忍者B後去的,發現頭目死了,那他接下來怎麼辦?算任務失敗還是算完成了?
當然忍者B最後肯定還是回去覆命了,也算他任務成功,這是任務本身的規則和秩序所決定的,但是程式的世界是無秩序,需要程式設計師通過程式碼去打造這個無序的世界從而形成秩序。
資源自身一定要包含約束性和規則性才能被正確的使用。
java本身提供了資源被多個執行緒排程的控制方式。
·synchronized關鍵字
·Atomic包
·ReentrantLock
·Semaphore
·CountDownLatch
·CyclicBarrier
·Phaser
我們通過一張圖表來概況瞭解一下。
對於資源的控制的方式無非就是一個“鎖” 字。現實當中到處充斥著這樣的例子,例如一個城市的市長,按照規定只能有一個,誰上任,那麼市長這個資源位就被誰“鎖”住了。但是程式世界中的“鎖”和現實世界中的“鎖”差別很大。
·現實世界的鎖,是可見,它控制著某一樣可見的物體,比如:門、箱子等,而再由這些具有隔絕性質的物體去控制級別更高的資源,例如:門裡的東西、箱子裡的錢。也就是說現實世界的鎖是間接的控制資源。
·程式世界裡的鎖,則是一種更為高階的抽象,它包含的對資源的各種維度的控制,我們可以將其理解為“規則”,比如:某類資源在同一時刻只能有一個執行緒進行操作(同步性)、某類資源必須由多個執行緒同時操作(同步協作)、某類資源最多隻能有N個執行緒進行操作(資源排程許可證)。這些都是“規則”的運用。
java中有關於鎖的內容非常多,我們這裡先用一張圖來簡要介紹一下,以後再著重篇幅去介紹每個鎖的相關特性。
1. 任何例項對外提供的方法(儘量避免對變數的直接操作)
2. 我們需要在對外提供的方法內用“鎖”去控制方法內的被排程的規則。
3. 實行第二條之前一定要確定當前的程式設計環境,是單執行緒的還是多執行緒
3.執行緒池
用一段話形容執行緒和資源的關係那就是。某個人(
)去做(
)某件有要求和規則(
)的事(
);根據這件事(
)的要求和規則(
)去約束做這個事人(
)的做法。
我們用各種鎖策略去保證資源能被正確的使用。這裡我們還缺一個角度,那就是從執行緒的角度去排程資源。
我們用一個問題開頭來展開對話。
·問:我們能根據資源的數量去建立執行緒的數量嗎?
·答:不能,因為建立執行緒的開銷大,受機器的配置的限制。
·問:那麼能不能建立一定數量的執行緒,去迴圈的排程資源。
·答:這麼做是可以的,但是資源數一般來說肯定是多於執行緒數,我們要控制資源的排程順序,還未來得及排程的資源可以按先來後到原則存放到佇列。
·問:那資源數少於執行緒數時候,該怎麼樣去處理。
·答:我們可以保留一定量的執行緒,為未來可能排程的資源做預備。
這就是一個執行緒池的雛形,執行緒池的雛形具有以下的基本特性
1. 具有最大的執行緒數限制
2. 有若干常備執行緒(核心執行緒)
3. 資源數若多超過了最大執行緒數的限制則會放入佇列中。
我們來解刨執行緒池的最全的配置資訊:
1. corePoolSize:核心執行緒數
2. maximumPoolSize:最大的執行緒數
3. keepAliveTime:無資源排程的執行緒回收的時間(預設單位:毫秒)
4. TimeUnit:時間單位
5. BlockingQueueRunnable:多餘的資源放入的佇列
6. ThreadFactory:執行緒的工廠類
7. RejectedExecutionHandler:執行緒池排程資源的策略
按照我們常規的設定
BlockingQueue maximumPoolSize ≥ corePoolSize
·corePoolSize會隨著資源排程數增加至maximumPoolSize
·當執行緒空閒時,會根據keepAliveTime來回收執行緒數(maximumPoolSize-corePoolSize)
·BlockingQueue分無界Queue和有界Queue。
·當資源排程大於maximumPoolSize時會放入BlockingQueue中
o 當BlockingQueue是有界佇列則存入
o 當BlockingQueue是無界佇列則根據策略調整
·當資源排程數大於BlockingQueue的長度,則根據RejectedExecutionHandler的策略來調整資源排程情況。
o AbortPolicy:預設策略,捨棄最新的資源排程,並丟擲異常
o DiscardPolicy:捨棄最新的資源排程,不會有異常
o DiscardOldestPolicy:捨棄在佇列中隊頭的資源
o CallerRunsPolicy:交由主執行緒去執行(慎用)
o 自定義拒絕策略:實現RejectedExecutionHandler介面,編寫特殊業務的拒絕策略。
總結:執行緒池就是多個執行緒來排程多個資源時所優化的一種多維度的策略,它的核心就是執行緒的複用以及資源的緩衝儲存。