第1章 并發(fā)編程的挑戰(zhàn)
1.1 上下文切換
即便是單核CPU也支持多線程并發(fā)残家,CPU通過給每個線程分配時間片(幾十毫秒)來實現(xiàn)并發(fā)的機制榆俺。通過不停切換線程,使得多個任務并發(fā)處理坞淮。任務從保存到再加載的過程就是一次上下文切換茴晋。由于上下文切換以及線程創(chuàng)建的開銷,可能會導致并發(fā)執(zhí)行的速度比串行執(zhí)行要慢回窘。
通過無鎖并發(fā)編程诺擅,CAS算法,使用最少線程和使用協(xié)程可以減少上下文切換啡直。
1.2 避免死鎖
避免死鎖的常見方法:
避免一個線程同時獲取多個鎖烁涌。
避免一個線程在鎖內占用多個資源,盡量保證每個鎖只占用一個資源酒觅。
嘗試使用定時鎖來代替內部鎖機制撮执。
對于數(shù)據(jù)庫鎖,加鎖和解鎖必須在一個連接里舷丹,否則會出現(xiàn)解鎖失敗的情況抒钱。
1.3 資源限制
資源限制是指在進行并發(fā)編程時,程序的執(zhí)行速度受限于硬件或軟件資源颜凯。例如帶寬谋币,硬盤讀寫,以及CPU處理速度症概,數(shù)據(jù)庫連接數(shù)等等蕾额。并發(fā)編程中,將代碼執(zhí)行速度加快的原則是將代碼中串行執(zhí)行的部分變成并行彼城。但如果受限于資源诅蝶,并發(fā)的代碼仍然會串行執(zhí)行。
通過集群或資源池復用可以幫助緩解資源限制問題募壕,根據(jù)不同的資源限制調整并發(fā)度调炬。
第2章 Java并發(fā)機制的底層實現(xiàn)原理
java代碼經(jīng)過編譯會變成字節(jié)碼,然后被類加載器加載到JVM中司抱,JVM執(zhí)行字節(jié)碼筐眷,最終轉化為匯編指令在CPU上執(zhí)行,而Java所使用的并發(fā)機制依賴于JVM的實現(xiàn)和CPU的指令习柠。
2.1 volatile的應用
volatile是一個輕量級的synchronized匀谣,在多CPU開發(fā)中保證了共享變量的“可見性”,也就是說當一個線程修改一個共享變量的時候资溃,另一個線程能夠讀取到所修改的值武翎。如果volatile使用恰當?shù)脑挘鼘⒈萻ynchronized的使用和執(zhí)行成本更低溶锭,不會引起上下文的切換和調度宝恶。
Java允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應該確保通過排它鎖單獨獲取這個變量垫毙。
如果一個變量被聲明為volatile霹疫,Java線程內存模型確保所有線程看到這個變量的值是一致的。
volatile修飾的共享變量在轉換為匯編語言后综芥,會出現(xiàn)Lock前綴指令丽蝎,該指令在多核處理器下引發(fā)了兩件事:
1、將當前處理器緩存行(CPU cache中可以分配的最小存儲單位)的數(shù)據(jù)寫回到系統(tǒng)內存膀藐。
2屠阻、這個寫回內存的操作使得其他CPU里緩存了該內存地址的數(shù)據(jù)無效。
為了提高處理速度额各,CPU不直接和內存通信国觉,而是將內存數(shù)據(jù)讀取到cache后進行操作,但何時寫回到內存是不確定的虾啦。如果線程volatile變量進行了寫操作麻诀,則JVM會向CPU發(fā)送一條Lock前綴指令,將該變量的所在的cache行的數(shù)據(jù)寫回到內存中缸逃。同時针饥,為了保證其他CPU所讀取到的cache值是一致的,就戶實現(xiàn)cache一致性協(xié)議需频,每個CPU通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己所緩存的值是否過期丁眼。如果CPU發(fā)現(xiàn)自己cache行中所對應的內存地址被修改,就會將該cache行設置為無效昭殉,從而在對該數(shù)據(jù)進行修改的時候重新從內存中讀取苞七。
volatile的兩條實現(xiàn)原則:
1、Lock前綴指令會引起CPU cache寫回到內存挪丢。
2蹂风、一個CPU的cache寫回到內存會導致其他處理器緩存無效。
2.2 synchronized的應用
synchronized實現(xiàn)同步的基礎是:Java中每個對象都可以作為鎖乾蓬,具體表現(xiàn)為以下三種形式:
1惠啄、對于普通同步方法,鎖是當前實例對象任内;
2撵渡、對于靜態(tài)同步方法,鎖是當前類的class對象死嗦;
3趋距、對于同步方法塊,鎖是synchronized括號里配置的對象
JVM基于進入和退出monitor對象來實現(xiàn)方法的同步和代碼塊同步越除,但是兩者細節(jié)不同节腐。代碼塊同步是使用monitorenter和monitorexit指令實現(xiàn)外盯。monitorenter和monitorexit指令是在編譯后插入到同步代碼塊開始和結束的的位置。任何一個對象都有一個monitor與之關聯(lián)翼雀,當一個monitor被持有之后饱苟,將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時锅纺,會嘗試獲取所有對象對應的monitor所有權掷空,也即獲得對象的鎖肋殴。
synchronized所用到的鎖是存在Java對象頭中囤锉。在Java1.6中,鎖一共有4種狀態(tài)护锤,由低到高依次是:無鎖官地,偏向鎖,輕量級鎖烙懦,重量級鎖驱入,這幾種狀態(tài)會隨著競爭情況逐漸升級。
CAS操作的意思是比較并交換氯析,它需要兩個數(shù)值亏较,一個舊值(期望操作前的值)和新值。操作之前比較兩個舊值是否變化掩缓,如無變化才交換為新值雪情。
在硬件層面,CPU依靠總線加鎖和緩存鎖定機制來實現(xiàn)原子操作你辣。
使用總線鎖保證原子性巡通。如果多個CPU同時對共享變量進行寫操作(i++),通常無法得到期望的值舍哄。CPU使用總線鎖來保證對共享變量寫操作的原子性宴凉,當CPU在總線上輸出LOCK信號時,其他CPU的請求將被阻塞住表悬,于是該CPU可以獨占共享內存弥锄。
使用緩存鎖保證原子性。頻繁使用的內存地址的數(shù)據(jù)會緩存于CPU的cache中蟆沫,那么原子操作只需在CPU內部執(zhí)行即可籽暇,不需要鎖住整個總線。緩存鎖是指在內存中的數(shù)據(jù)如果被緩存于CPU的cache中饥追,并且在LOCK操作期間被鎖定图仓,那么當它執(zhí)行鎖操作寫回到內存時,CPU不使用總線鎖但绕,而是修改內部的內存地址救崔,并允許它的cache一致性來保證操作的原子性惶看,當其他CPU回寫被鎖定的cache行數(shù)據(jù)時候,會使cache行無效六孵。
Java使用了鎖和循環(huán)CAS的方式來實現(xiàn)原子操作纬黎。
使用循環(huán)CAS實現(xiàn)原子操作。JVM的CAS操作使用了CPU提供的CMPXCHG指令來實現(xiàn)劫窒,自旋式CAS操作的基本思路是循環(huán)進行CAS操作直到成功為止本今。1.5之后的并發(fā)包中提供了諸如AtomicBoolean, AtomicInteger等包裝類來支持原子操作。CAS存在ABA問題主巍,循環(huán)時間長開銷大冠息,以及只能保證一個共享變量的原子操作。
使用鎖機制實現(xiàn)原子操作孕索。鎖機制保證了只有獲得鎖的線程才能給操作鎖定的區(qū)域逛艰。JVM的內部實現(xiàn)了多種鎖機制。除了偏向鎖搞旭,其他鎖的方式都使用了循環(huán)CAS散怖,也就是當一個線程想進入同步塊的時候,使用循環(huán)CAS方式來獲取鎖肄渗,退出時使用CAS來釋放鎖镇眷。
第三章 Java內存模型
Java內存模型基礎
在并發(fā)編程中,線程間如何通信和線程見如何同步時需要處理的兩個問題翎嫡。在命令式的編程中欠动,線程之間的通信主要依靠內存共享和消息傳遞。同步時指程序中用于控制不同線程之間操作發(fā)生相對順序的機制钝的。在共享內存并發(fā)模型中翁垂,需要顯式指定某個方法或代碼需要在線程之間互斥執(zhí)行。
在Java中硝桩,堆內存在線程之間共享沿猜,線程之間的通信由Java內存模型JMM控制。線程之間的共享變量存儲在主內存中碗脊,每個線程都有一個私有的本地內存(并不真實存在)啼肩,本地內存中存儲了線程讀寫共享變量的副本。
在執(zhí)行程序時衙伶,為了提高性能祈坠,編譯器和CPU常常會對指令進行重排序,分為以下3種類型:
1矢劲、編譯優(yōu)化重排序赦拘。編譯器在不改變單線程程序語義的前提下,可以重新安排語句執(zhí)行順序芬沉。
2躺同、指令級并行的重排序阁猜。CPU采用了指令級并行技術將多條指令重疊執(zhí)行。
3蹋艺、內存系統(tǒng)的重排序剃袍。由于CPU使用cache和讀/寫緩沖區(qū),因此加載和存儲操作可能在亂序執(zhí)行捎谨。
Java源代碼到最終實際執(zhí)行的指令序列民效,會分別經(jīng)過上述3種重排序。這些重排序可能會導致多線程程序出現(xiàn)內存可見性問題涛救。JMM屬于語言級別的內存模型畏邢,確保在不同編譯器和不同CPU平臺之上,通過禁止特定類型的編譯器重排序和CPU重排序州叠,為開發(fā)者提供一致的內存可見性保證棵红。
現(xiàn)代CPU通過cache來保存向內存中寫入的數(shù)據(jù)。cache可以保證指令流水線持續(xù)運行咧栗,可以避免由于CPU停頓下來等待向內存寫入數(shù)據(jù)而產(chǎn)生的延遲。通過批處理的方式刷新cache虱肄,合并寫cache對于同一內存地址的多次寫操作致板,可以減少總線的占用。
然而咏窿,每個CPU的cache只對它所在的CPU可見斟或,這將會導致出現(xiàn)對內存的讀寫并發(fā)問題。因此集嵌,CPU都會允許對W-R操作進行重排序萝挤。為了保證內存可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型CPU重排序根欧。
JDK1.5后怜珍,Java采用JSR133內存模型。通過happens-before概念來闡述操作之間的內存可見性凤粗。在JMM中酥泛,如果一個操作執(zhí)行的結果要對另一個操作可見,那么這兩個操作之間必須要有happens-before關系嫌拣。這兩個操作可以在同一個線程中柔袁,也可以在不同的線程中。(兩個操作之間具有happens-before關系异逐,不意味著前一個操作必須要在后一個操作之前執(zhí)行捶索,僅僅要求前一個操作的執(zhí)行結果對后一個操作可見)
重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。對于單個CPU和單個線程中所執(zhí)行的操作而言灰瞻,如果兩個操作都訪問了一個變量腥例,且兩個操作中有寫操作燥筷,那么這兩個操作就具有依賴性。(RW,WW,WR)這三種操作只要重排序對操作的執(zhí)行順序院崇,程序的執(zhí)行結果就會被改變肆氓,因此,編譯器和處理器在進行重排序的時候會遵守數(shù)據(jù)依賴性底瓣,不會改變存在數(shù)據(jù)依賴關系的兩個操作的執(zhí)行順序谢揪。
as-if-serial:無論如何重排序,(單線程)程序的執(zhí)行結果不能被改變捐凭。
編譯器拨扶,runtime,CPU都必須遵守as-if-serial語義茁肠,因此患民,編譯器和CPU不會對存在數(shù)據(jù)依賴關系的操作進行重排序。
在單線程中垦梆,對存在控制依賴性的操作進行重排序匹颤,不會改變執(zhí)行結果,而在多線程中則可能會改變結果托猩。
順序一致性
程序未正確同步的時候印蓖,就可能存在數(shù)據(jù)競爭:
在一個線程中寫一個變量,在另一個線程中讀同一個變量京腥,而且寫和讀沒有通過同步來排序赦肃。
JMM對正確同步的多線程程序的內存一致性做了如下保證:
如果程序是正確同步的,程序的執(zhí)行將具有順序一致性公浪,程序的執(zhí)行結果與該程序的順序一致性內存模型的執(zhí)行結果相同他宛。JMM中,臨界區(qū)內的代碼可以重排序欠气。而對于未正確同步的多線程程序厅各,JMM只提供最小的安全性:線程執(zhí)行時所讀取到的值,要么是之前某個線程所寫入的值晃琳,要么是默認值讯检。
volatile的內存語義
一個volatile變量的單個R/W操作,與一個普通變量的R/W操作使用同一個鎖來同步卫旱,它們的執(zhí)行效果相同人灼。鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個線程之間的內存可見性,這也意味著對一個volatile變量的R操作顾翼,總是能看到任意線程對該變量最后的寫入投放。
對于volatile變量本身的單個RW操作具有原子性,但是與鎖不同的是适贸,多個對于volatile變量的復合操作不具有原子性灸芳。而鎖的語義保證了臨界區(qū)代碼的執(zhí)行具有原子性涝桅。
JAVA1.5后,JSR-133增強了volatile的內存語義烙样,嚴格限制編譯器和CPU對于volatile變量與普通變量的重排序冯遂,從而確保volatile變量的W-R操作可以實現(xiàn)線程之間的通信,提供了一種比鎖更輕量級的線程通信機制谒获。從內存語義的角度而言蛤肌,volatile的W-R與鎖的釋放-獲取有相同的內存效果:W操作=鎖的釋放;R操作=鎖的獲取批狱。
A線程寫一個volatile變量x后裸准,B線程讀取x以及其他共享變量。
1. 當A線程對x進行寫操作時赔硫,JMM會把該線程A對應的cache中的共享變量值刷新到主存中.(實質上是線程A向接下來要讀變量x的線程發(fā)出了其對共享變量修改的消息)
2.當B線程對x進行讀取時炒俱,JMM會把該線程對應的cache值設置為無效,而從主存中讀取x爪膊。(實質上是線程B接收了某個線程發(fā)出的對共享變量修改的消息)
兩個步驟綜合起來看权悟,在線程B讀取一個volatile變量x后,線程A本地cache中在寫這個變量x之前所有其他可見的共享變量的值都立即變得對B可見惊完。線程A寫volatile變量x僵芹,B讀x的過程實質上是線程A通過主存向B發(fā)送消息。
需要注意的是小槐,由于volatile僅僅保證對單個volatile變量的R/W操作具有原子性,而鎖的互斥則可以確保整個臨界區(qū)代碼執(zhí)行的原子性荷辕。(參見《Java理論與實踐:正確使用volatile變量》)
鎖的內存語義
鎖是Java編程中最重要的同步機制凿跳,除了讓臨界區(qū)互斥執(zhí)行之外,還可以讓釋放鎖的線程向獲取鎖的線程發(fā)送消息疮方。當線程釋放鎖時控嗜,JMM會把該線程對應的本地cache中的共享變量刷新到主存中。當線程獲取鎖時骡显,JMM會把該線程對應的本地內存置為無效疆栏,從而使得臨界區(qū)的代碼必須從主存中讀取共享變量。
對比鎖和volatile的內存語義可以看出:鎖的釋放與volatile的寫操作有相同的內存語義惫谤,鎖的獲取與volatile的讀操作有相同的內存語義壁顶。
第四章 Java并發(fā)編程基礎
線程作為操作系統(tǒng)調度的最小單元,都擁有各自的計數(shù)器溜歪,堆棧和局部變量等屬性若专,并且能夠訪問共享的內存變量。
不同的JVM以及OS上蝴猪,線程規(guī)劃會存在差異调衰,有些OS甚至會忽略對線程優(yōu)先級的設定膊爪。
Java線程在生命周期中可以處于6種不同的狀態(tài):
NEW:初始狀態(tài),線程被構建嚎莉,但沒有調用start方法
RUNNABLE:運行狀態(tài)米酬,Java線程將操作系統(tǒng)中的就緒和運行兩種狀態(tài)統(tǒng)稱為運行中
BLOCKED:阻塞狀態(tài),表示線程阻塞于鎖
WAITING:等待狀態(tài)趋箩,表示當前線程需要等待其他線程做出特定的通知或中斷
TIME_WAITING:超時等待狀態(tài)赃额,它可以在指定時間自行返回
TERMINATED:終止狀態(tài),表示當前線程以及執(zhí)行完畢
Deamon線程是一種支持型線程阁簸,因為它主要被用作程序中后臺調度以及支持性工作爬早。當JVM中不存在非Deamon線程的時候,JVM將會退出启妹。因此筛严,在構建Deamon線程時,不能依靠finally來確保執(zhí)行關閉或者清理資源的邏輯饶米。
啟動和終止線程
在運行線程之前首先要構造一個線程的對象桨啃,并提供所需的屬性。一個新構建的線程對象是由其parent線程進行空間分配的檬输,而子線程繼承了parent線程是否為Deamon線程照瘾,優(yōu)先級,以及threadLocal等丧慈。
線程的中斷狀態(tài)是線程的一個標識位析命,表示一個運行中的線程是否被其他線程進行了中斷操作,也是一種簡便的線程間交互方式逃默,適合用來取消或停止任務鹃愤。通過中斷或標識位的方式對任務進行終止更加安全。
線程間通信
Java 線程在運行過程中擁有自己的椡暧颍空間,為了提高程序的執(zhí)行速度软吐,線程在棧空間內保存了變量的副本吟税。volatile保證了所有線程對其所修飾的成員變量的可見性凹耙,synchronized可以修飾方法或同步塊,保證了線程對變量訪問的可見性和排他性肠仪。同步塊的實現(xiàn)采用了monitorenter和monitorexit指令肖抱,而同步塊方法則依靠方法修飾符的ACC_SYNCHRONIZED來完成。但其本質上都是采用了對一個對象的監(jiān)視器進行獲取藤韵,這個過程是排他的虐沥。
任何一個對象都擁有自己的monitor,這個對象被同步塊或者該對象的同步方法調用時,執(zhí)行方法的線程必須獲取到該對象的monitor欲险,這個獲取過程是排他的镐依。未獲取到monitor的線程將會被阻塞于同步塊或方法的入口處,進入BLOCKED狀態(tài)天试。
等待/通知機制槐壳,是指一個線程A調用了對象O的wait方法進入了等待狀態(tài),而另一個線程B調用了對象O的notify方法或者notifyAll方法喜每,線程A收到通知后從對象O的wait方法返回务唐,進而執(zhí)行后續(xù)操作。
在調用對象的wait带兜,notify枫笛,notifyAll方法之前要對該對象加鎖,調用對象O的wait方法后會釋放對象O的鎖刚照。在調用notify和notifyAll方法后刑巧,等待線程并不會返回,需要通知線程釋放鎖之后才能從wait方法返回无畔。notify方法將等待線程從等待隊列放入同步隊列啊楚,線程由WAITING狀態(tài)進入BLOCKED狀態(tài)。
管道的IO流與普通文件IO流的不同浑彰,它主要用于線程之間的數(shù)據(jù)傳輸恭理。
如果一個線程A執(zhí)行了B.join方法,則線程A直到B線程終止后才從該方法中返回郭变。
第五章 Java中的鎖
鎖是用來控制多個線程訪問共享資源的方式颜价。Lock接口提供了顯式獲取和釋放鎖的方式的同時,也提供了非阻塞性獲取鎖诉濒,中斷鎖以及超時鎖等synchronized不具備的特性拍嵌。
隊列同步器是用來構建鎖或其他同步組件的基礎框架,其主要使用方式是繼承循诉。它使用一個int成員變量表示同步狀態(tài),通過內置的FIFO隊列來完成資源獲取線程的排隊工作撇他。
同步器是實現(xiàn)鎖以及其他同步組件的關鍵茄猫。鎖是面向使用者的,定義了使用者與鎖交互的接口困肩,隱藏了實現(xiàn)細節(jié)划纽。同步器面向的是鎖的實現(xiàn)者,簡化了鎖的實現(xiàn)方式锌畸,實現(xiàn)了鎖的語義勇劣。
重入鎖表示該鎖能夠支持一個線程對資源重復加鎖,還支持獲取鎖時候的公平性選擇。公平鎖保證了鎖按照FIFO原則比默,而代價是大量的線程切換幻捏。
獨占鎖和重入鎖都是排他鎖,同一時刻只允許一個線程訪問臨界區(qū)命咐。而讀寫鎖在同一時刻可以允許多個線程訪問篡九,但在寫線程訪問時,其他讀寫線程均被阻塞醋奠。讀寫鎖維護了一個寫鎖和一個讀鎖榛臼,通過分離讀鎖和寫鎖使并發(fā)性比一般排他鎖有了更大的提升。
任意Java對象窜司,都擁有一組監(jiān)視器方法沛善,主要包括wait,notify塞祈,notifyAll方法金刁,這些方法與synchronized配合可以實現(xiàn)等待/通知模式。而Condition接口同樣提供了類似Object的監(jiān)視器方法织咧,與Lock配合同樣可以實現(xiàn)該模式胀葱。
第六章 Java并發(fā)容器和框架
ConcurrentHashMap
并發(fā)編程中HashMap不是線程安全的容器,多線程會導致HashMap的Entry鏈表形成環(huán)形數(shù)據(jù)結構笙蒙,在并發(fā)執(zhí)行put操作時會引起死循環(huán)抵屿。
HashTable使用synchronized來保證線程安全,在線程競爭激烈的情況下效率不高捅位。一個線程訪問HashTable的同步方法時轧葛,其他訪問線程必須競爭同一把鎖,會進入阻塞或輪詢狀態(tài)艇搀。
ConcurrentHashMap使用鎖分段的技術尿扯,將數(shù)據(jù)分成一段一段地存儲,每段數(shù)據(jù)配一把鎖焰雕,當一個線程訪問一個數(shù)據(jù)段時其他數(shù)據(jù)段也可以被其他線程訪問衷笋。
ConcurrentHashMap由Segment數(shù)組結構和HashEntry數(shù)組結構組成。Segment是一個鐘可重入鎖矩屁,而HashEntry則用于存儲鍵值對數(shù)據(jù)辟宗。一個ConcurrentHashMap里包含了一個Segment數(shù)組,與HashMap類似吝秕,Segment是一種數(shù)組和鏈表結構泊脐。每個Segment中包含了一個HashEntry數(shù)組,HashEntry是一個鏈表結構的元素烁峭。當對HashEntry中元素進行修改時容客,需要獲得與之對應的Segment鎖秕铛。
ConcurrentLinkedQueue
在并發(fā)編程中,實現(xiàn)一個安全隊列的方法有兩種:阻塞算法和非阻塞算法缩挑。阻塞算法利用鎖來保證隊列的并發(fā)操作但两,而非阻塞算法使用循環(huán)CAS的方式來實現(xiàn)。ConcurrentLinkedQueue采用了非阻塞的方式來實現(xiàn)调煎。
第八章 Java中的并發(fā)工具類
CountDownLatch
join方法用于讓當前線程等待join線程執(zhí)行結束镜遣,其原理是不停檢查join線程是否存活,直到join線程終止士袄。CountDownLatch作為計數(shù)器也可以實現(xiàn)join的功能悲关,當調用countDown方法時計數(shù)器減1,并調用await方法阻塞當前線程娄柳,直到計數(shù)器為0.
CyclicBarrier
CyclicBarrier能夠讓一組線程達到一個屏障(同步點)時候被阻塞寓辱,直到最后一個線程到達屏障后,所有被阻塞的線程才能繼續(xù)執(zhí)行赤拒。它可以用于多線程計算數(shù)據(jù)秫筏,最后合并計算結果的場景。
CountDownLatch的計數(shù)器只能使用一次挎挖,而CyclicBarrier可以使用reset方法重置这敬。
Semaphore
Semaphore信號量是用來控制同時訪問特定資源的線程數(shù)量,通過協(xié)調各個線程保證合理使用公共資源蕉朵,可以用于流量控制崔涂,例如數(shù)據(jù)庫連接。
第九章 Java中的線程池
Java中的線程池可以降低資源消耗始衅,提高響應速度冷蚂,提高線程的可管理性。當一個新任務提交到線程池后汛闸,處理流程如下:
1蝙茶、線程池判斷核心線程池里的線程是否都在執(zhí)行任務。如果不是诸老,則創(chuàng)建一個新的工作線程來執(zhí)行任務隆夯;如果核心線程池已滿,則進入下一步别伏。()
2吮廉、線程池判斷工作隊列是否已滿。如果工作隊列未滿畸肆,則將新任務存儲于工作隊列中。如果工作隊列也滿了宙址,則進入下一步轴脐。
3、線程池判斷線程池中的線程是否都處于工作狀態(tài)。如果沒有大咱,則創(chuàng)建一個新的工作線程來執(zhí)行任務恬涧,如果線程池已滿,則交給飽和策略處理該任務碴巾。
要想合理配置線程池溯捆,就必須首先分析任務特性:
任務性質:CPU密集型,IO密集型厦瓢,混合任務提揍。
優(yōu)先級:高,中煮仇,低劳跃。
任務執(zhí)行時間:長,中浙垫,短刨仑。
任務的依賴性:是否依賴其他系統(tǒng)資源,例如數(shù)據(jù)庫連接夹姥。
性質不同的任務可以用不同規(guī)模的線程池分開處理杉武。CPU密集型的任務應配置盡可能小的線程數(shù),如配置核數(shù)N+1個線程的線程池辙售。對于IO密集型的任務轻抱,CPU并不是一直在執(zhí)行,則應該配置盡可能多的線程數(shù)圾亏,例如2*N十拣。對于混合型任務,如果可以拆分任務且任務執(zhí)行時間差不多志鹃,則將任務拆成不同性質的小任務來執(zhí)行夭问。優(yōu)先級不同的任務可以使用優(yōu)先級隊列PriorityBlockingQueue來處理,讓優(yōu)先級高的任務先執(zhí)行曹铃。執(zhí)行時間不同的任務可以交給不同規(guī)模的線程池來處理缰趋,或者使用優(yōu)先級隊列來讓時間短的任務先執(zhí)行。依賴數(shù)據(jù)庫連接池的任務陕见。等待時間越長秘血,CPU閑置時間越長,則線程池線程數(shù)目越大越能更好利用CPU评甜。
第10章 Executor框架
Java的線程既是工作單元(runnable灰粮,callable)也是執(zhí)行機制(Executor)。
Java多線程程序通常將應用分解為若干個任務忍坷,然后使用Executor框架控制上層調度粘舟,而下層調度由操作系統(tǒng)內核控制熔脂。Executor框架由3大部分組成:
任務,被執(zhí)行任務需要實現(xiàn)接口:Runnable或Callable接口柑肴。
任務的執(zhí)行霞揉,包括任務執(zhí)行機制的核心接口Executor,以及繼承自Executor的ExecutorService接口晰骑。Executor框架中ThreadPoolExecutor和ScheduledThreadPoolExecutor類實現(xiàn)了ExecutorService接口适秩。
異步計算結果伤靠,包括接口Feature和實現(xiàn)了Feature接口的FeatureTask類柒室。
Runnable或Callable接口的實現(xiàn)類都可以被線程池執(zhí)行匙铡,Runnable無法返回結果堕伪,Callable可以返回結果该窗。
Executor接口是Executor框架的基礎褐荷,它將任務的提交與任務的執(zhí)行分離開來腻菇。它的實現(xiàn)類ThreadPoolExecutor和ScheduledThreadPoolExecutor都由使用工廠類Executors來創(chuàng)建作岖。
ThreadPoolExecutor:
FixedThreadPool:固定線程數(shù)的線程池耗式。適用于為了滿足資源管理的需求而需要限制當前數(shù)量的應用場景胁住,例如負載較重的服務器。
SingleThreadPoolExecutor:單個線程的線程池刊咳。適用于需要保證順序地執(zhí)行各個任務彪见。
CachedThreadPool:能夠根據(jù)需要創(chuàng)建新線程的線程池。大小無界娱挨,適用于執(zhí)行很多短期異步的小程序余指,或者負載較輕的服務器。
ScheduledThreadPoolExecutor可以給定延遲后運行命令跷坝。
ScheduledThreadPoolExecutor:固定線程數(shù)的線程池酵镜。適用于需要多個后臺線程執(zhí)行周期任務,同時為了滿足資源管理需求限制后臺線程數(shù)量的場景柴钻。
SingleScheduledThreadPoolExecutor:適用于需要單個后臺線程執(zhí)行周期任務淮韭,同時需要包裝順序執(zhí)行各個任務的應用場景。
Feature接口及其實現(xiàn)類FeatureTask用來表示異步計算的結果贴届。當把任務提交給線程池后靠粪,線程池會返回FeatureTask對象。通過執(zhí)行FeatureTask.get方法來等待任務執(zhí)行完成毫蚓,完成后該方法返回任務執(zhí)行的結果占键。