每一個(gè)想學(xué)習(xí)Java多線程的人,手里至少有這本書或者至少要看這本書。強(qiáng)烈建議大家多看幾遍。
代碼中比較容易出現(xiàn)bug的場景:
不一致的同步,直接調(diào)用Thread.run,未被釋放的鎖,空的同步塊,雙重檢查加鎖,在構(gòu)造函數(shù)中啟動一個(gè)線程,notify或notifyAll通知錯誤,Object.wait和Condition.await未在同步方法或塊中調(diào)用,把Lock當(dāng)鎖用,調(diào)用Condition.wait方法,在休眠或等待時(shí)持有鎖,自旋循環(huán)。
1.多線程可以提高資源的利用率,可以充分利用現(xiàn)代多核處理器的特性,讓每個(gè)線程負(fù)責(zé)處理同類型的任務(wù),更加容易維護(hù),同時(shí)通過異步處理提高響應(yīng)性。
2.多線程之間為更方便的實(shí)現(xiàn)數(shù)據(jù)共享采用了共享相同內(nèi)存地址空間的形式,并且是并發(fā)運(yùn)行,導(dǎo)致多個(gè)線程可能會同時(shí)訪問或修改其他線程正在使用的變量值,導(dǎo)致安全性,同時(shí)如果線程之間相互等待對方擁有的鎖,會出現(xiàn)活躍性即死鎖問題。如果線程計(jì)算部分不多,更多的線程只會導(dǎo)致頻繁的切換上下文,讓CPU的時(shí)間更多的花在線程調(diào)度而不是任務(wù)執(zhí)行上。
3.java同步的幾種方式:synchronized,volatile,顯示鎖,原子變量,線程及對象的基礎(chǔ)同步方法。
4.所謂線程安全就是當(dāng)多個(gè)線程訪問某個(gè)類時(shí),不管運(yùn)行時(shí)環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個(gè)類都能表現(xiàn)出正確的行為。
5.將復(fù)合操作放在一個(gè)原子操作中執(zhí)行,或用相同的鎖來保護(hù)每個(gè)共享的和可變的變量。
6.增加同步必然會導(dǎo)致代碼的復(fù)雜性,為性能犧牲代碼簡單性時(shí)不要太盲目,因?yàn)樵綇?fù)雜的代碼,其不安全性越大。
7.當(dāng)執(zhí)行時(shí)間較長的計(jì)算或可能無法快速完成的操作時(shí),如網(wǎng)絡(luò)I/O,一定不要持有鎖。
8.線程之間變量的讀取,在沒有同步的情況下,編譯器,處理器,以及運(yùn)行時(shí)都有可能對部分指令進(jìn)行重排導(dǎo)致并發(fā)問題。日常開發(fā)中常見的set/get,如果沒有都加上synchronized,在多線程環(huán)境下也存在同樣的問題。
9.對于非volatile的64位long或double,由于JVM允許對他們的讀取分解為高低32位來讀取,多線程下會發(fā)生只讀取部分32位的問題,因此對這些變量,要用volatile或鎖保護(hù)讀取。
10.對volatile變量,編譯器和運(yùn)行時(shí)不會將該變量上的操作與其他內(nèi)存操作一起重排序,也不會被緩存在寄存器或者對其他處理器不可見的地方,而是直接同步到內(nèi)存,保證其他線程讀取的時(shí)候返回最新寫入的值。確切的說,volatile變量只保證可見性,對于自增或自減的操作并不能保證其原子性,因此不是線程安全的。因此不要過多的依賴此對象,最好在滿足以下全部三個(gè)條件的情況下才考慮使用:
?(a)對變量的寫入不依賴當(dāng)前變量的值,即所謂的不是自增或自減情況,或者可以保證只有單個(gè)線程對其更新
?(b)該變量不參與到不變性條件的判斷
?(c)訪問該變量時(shí)不用加鎖
另一方面:當(dāng)且僅當(dāng)一個(gè)變量參與到包含其他狀態(tài)變量的不變性條件時(shí),才可以聲明為volatile類型。
11.發(fā)布對象的幾種方式:1.將對象的引用保存到其他代碼中或public域中。?2.非私有方法返回該對象引用或該對象引用作為參數(shù)傳遞。?3.發(fā)布Collections組合. 4.通過已發(fā)布對象的非私有變量引用或方法獲取到的對象?5.類的內(nèi)部類實(shí)例隱含的包含了對該類實(shí)例的引用。
12.正確的或安全的發(fā)布一個(gè)對象,即是保證對象的引用以及對象的狀態(tài)必須同時(shí)對其他線程可見。不正確的發(fā)布可變對象會導(dǎo)致線程安全問題,以下是一些保證變量或?qū)ο缶€程安全的方法:
??1.不要在構(gòu)造過程中使this逸出,即不要在構(gòu)造函數(shù)中創(chuàng)建并啟動線程,不要調(diào)用可改寫的方法,或注冊事件監(jiān)聽或?qū)?nèi)部類實(shí)例化。?2.多使用線程封閉,即盡量把對象放在單線程中不參與共享.?3.使用棧封閉,即在方法內(nèi)部用局部變量訪問對象。?4.用ThreadLocal封裝變量,為每個(gè)線程提供一個(gè)只屬于該線程的變量副本。可以視ThreadLocal<T>為Map<Thread,T>,另外還有一個(gè)好處就是當(dāng)線程終止后,該值也會被回收。(缺點(diǎn):ThreadLocal變量類似全局變量,會降低代碼的可重用性,并在類之間引入隱含的耦合性)。?5.多用不可變對象(對象正確創(chuàng)建未this逸出,且創(chuàng)建后其狀態(tài)不能修改,且所有域都是final),其一定是線程安全的。?6.在靜態(tài)初始化函數(shù)中初始化一個(gè)對象引用(JVM內(nèi)部的同步機(jī)制保證了這種發(fā)布方式的安全性) 7.將對象的引用保存到volatile類型的域,AtomicReferance對象,某個(gè)正確構(gòu)造對象的final域,或一個(gè)由鎖保護(hù)的域中。?8.將對象放入線程安全的容器中可以由容器內(nèi)部的同步機(jī)制保障對象安全發(fā)布。
13.如果需要對一組數(shù)據(jù)以原子方式執(zhí)行某個(gè)操作,為避免競態(tài)條件,可以創(chuàng)建一個(gè)不可變類來包含這些數(shù)據(jù)(如果數(shù)據(jù)是數(shù)組或其他可變對象,該類對應(yīng)的變量為clone的副本以保證不可變性),通過把這些數(shù)據(jù)保存到該不可變類的實(shí)例上,并且用volatile來確保該實(shí)例的可見性,這樣可以保證線程操作數(shù)據(jù)的安全。如果該類對象是可變的,當(dāng)然可以加鎖來確保原子性。
14.使用同步和封轉(zhuǎn)來保護(hù)對象狀態(tài)即變量的不變性條件及后驗(yàn)條件,使得相關(guān)變量必須在單個(gè)原子操作中進(jìn)行讀取或更新。換句話說,借助原子性與封裝性,滿足狀態(tài)變量的有效值或狀態(tài)轉(zhuǎn)化上的各種約束條件,使得狀態(tài)變量有效轉(zhuǎn)換,是確保線程安全的有效手段。
15.實(shí)例封閉即是將一個(gè)對象的所有訪問代碼路徑都封裝到另一個(gè)對象里,可以通過類私有變量,局部變量,單個(gè)線程里等方式,保證被封閉的對象不會逸出,不會超出它們既定的作用域。常見的例子如同步包裝器工作如Collections.synchronizedList對容器對象的唯一引用以實(shí)現(xiàn)將底層容器對象封閉從而達(dá)到線程安全的目的。
16.委托現(xiàn)有的同步容器來保障線程安全一般對針對"面"上的存取,如果類身包含復(fù)合操作,則該類必須自己提供加鎖機(jī)制來保證這些復(fù)合操作的原子性。
17.當(dāng)為現(xiàn)有的類添加一個(gè)原子操作時(shí),利用組合并用同一個(gè)鎖來保護(hù)同步操作可以實(shí)現(xiàn)。
18.同步容器類:Vector,Hashtable,以及由Collections.synchronizedXxx等工廠方法包裝的同步封裝器類,它們實(shí)現(xiàn)線程安全的方式是:將它們的狀態(tài)封裝起來,并對每個(gè)公有方法都進(jìn)行同步,使得每次只有一個(gè)線程能訪問容器的狀態(tài)。因此如果基于這些共有方法衍生出了一些新操作,必須注意這些操作有可能不是原子的從而引發(fā)同步問題。
缺點(diǎn):同步容器將所有對容器狀態(tài)的訪問都串行化以實(shí)現(xiàn)它們的線程安全目的,因此當(dāng)多個(gè)線程同時(shí)競爭鎖時(shí),吞吐量將嚴(yán)重降低,并發(fā)性能嚴(yán)重受到影響。(正是由于上述原因,java5后開始提供多種并發(fā)容器來代替同步容器,以極大地提高伸縮性并降低風(fēng)險(xiǎn))
19.并發(fā)容器類:java5提供了多種并發(fā)容器以代替同步容器的低并發(fā)性,并增加了一些常見的復(fù)合操作,如if-not-add,替換,以及條件刪除,使得這些復(fù)合操作原子化。列舉及大致說明如下:
?a:ConcurrentHashMap:也是基于散列的Map,利用粒度更細(xì)的分段鎖機(jī)制使得任意數(shù)量的讀取線程可以并發(fā)訪問Map,并使得一定數(shù)量的寫入線程可以并發(fā)的修改Map,從而在并發(fā)訪問下實(shí)現(xiàn)更高的吞吐量。
?b:CopyOnWriteArrayList(Set):保留一個(gè)指向底層基礎(chǔ)數(shù)組的引用,每次修改對象時(shí),都會復(fù)制創(chuàng)建并重新發(fā)布一個(gè)新的容器副本,由于復(fù)制底層數(shù)組需要一定的開銷,因此這些容器僅適用于迭代操作遠(yuǎn)遠(yuǎn)多于修改操作的場景。
?c:Queue:用來保存一組等待處理的元素,如傳統(tǒng)FIFO的ConcurrentLinkedQueue,(非同步的)優(yōu)先隊(duì)列PriorityQueue,還有其他的,可參見API
?d:BlockingQueue:可阻塞的Queue,如LinkedBlockingQueue,ArrayBlockingQueue,PriorityBlockingQueue,以及無存儲容量的SynchronousQueue(該容器的put和take方法會一致堵塞,直到有另一個(gè)線程已經(jīng)準(zhǔn)備好參與到交付過程,因此僅當(dāng)有足夠多的消費(fèi)者,并且總是有一個(gè)消費(fèi)者準(zhǔn)備好獲取交付工作時(shí),才適合使用此同步隊(duì)列)。所有這些阻塞隊(duì)列適用于生產(chǎn)者-消費(fèi)者模式。
?e:Deque及BlockingDeque:java6新增的雙端及可阻塞雙端隊(duì)列,適用于工作密取模式。每個(gè)消費(fèi)者擁有各自的雙端隊(duì)列,當(dāng)完成自己的隊(duì)列是可以從其他隊(duì)列的尾部開始獲取工作,從而減少競爭提供并發(fā)。
20.在同步容器顯式迭代(for-each,Iterator)或隱式迭代(toString,hashCode,equals,contailsAll,removeAll,retainAll)過程中,修改容器會出現(xiàn)ConcurrentModificationExcepiton異常。但是在并發(fā)容器中,它們提供的迭代器不會拋出ConcurrentModificationException異常,因此不需要在迭代過程中對容器加鎖。
21.同步工具類:它們提供了一些特定的結(jié)構(gòu)化屬性,封裝了一些決定線程等待還是執(zhí)行的狀態(tài),并提供了操作這些狀態(tài)以及高效的等待同步工具類進(jìn)入預(yù)期狀態(tài)的方法。主要包括:
?a:CountDownLatch閉鎖
?b:FutureTask
?c:Semaphore信號量
?d:CyclicBarrier柵欄
22.并發(fā)任務(wù)的抽象,首先是要找到單個(gè)任務(wù)的邊界,盡量使得各任務(wù)相互獨(dú)立,任務(wù)之間不相互依賴,大多數(shù)服務(wù)器應(yīng)用都采用了自然的任務(wù)邊界,即以獨(dú)立的客戶請求為邊界。一般來說每項(xiàng)任務(wù)還應(yīng)該表示應(yīng)用程序的一小部分處理能力,從而使整個(gè)應(yīng)用程序表現(xiàn)出更好的吞吐量和響應(yīng)性。
23.如果任務(wù)的執(zhí)行時(shí)間較長,創(chuàng)建過多的線程不僅會耗費(fèi)JVM時(shí)間,消耗更多的內(nèi)存資源,而且大量的線程將競爭CPU的有限資源,另外線程棧的地址空間也會限制創(chuàng)建過多的線程。
24.各種線程池的創(chuàng)建方式及各自的一些特點(diǎn):
?a)newFixedThreadPool:固定長度的線程池,如果某個(gè)線程發(fā)生了未預(yù)期的Exception而結(jié)束,線程池會補(bǔ)償一個(gè)新的線程。
?b)newCachedThreadPool:動態(tài)可緩存的線程池,如果當(dāng)前線程池規(guī)模超過處理需求,則回收空閑線程,否則添加新線程。
?c)newSingleThreadExecuto:創(chuàng)建單個(gè)工作者線程來執(zhí)行任務(wù),如果這個(gè)線程異常結(jié)束,則會創(chuàng)建另一個(gè)線程來替代??梢源_保依照任務(wù)在隊(duì)列中的順序串行執(zhí)行(FIFO,LIFO,優(yōu)先級等)
?d)newScheduledThreadPool:以延時(shí)或定時(shí)方式來執(zhí)行任務(wù)的固定長度線程池。
25.ExecutorService擴(kuò)展了Executor接口以提供解決執(zhí)行服務(wù)生命周期的問題,ExecutorService生命周期有三種狀態(tài):運(yùn)行,關(guān)閉和已終止。shutdown方法平緩關(guān)閉:不再接受新的任務(wù),并等待已經(jīng)提交的以及尚未開始執(zhí)行的任務(wù)執(zhí)行完成。?shutdownNow方法粗暴關(guān)閉:嘗試取消所有運(yùn)行中的任務(wù),并且丟棄隊(duì)列中尚未開始執(zhí)行的任務(wù)。
26.ExecutorService關(guān)閉后提交的任務(wù)交由Rejected Execution Handler來處理,它會拋棄任務(wù)或使得execute方法拋出一個(gè)未檢查的RejectedExecutionException??梢哉{(diào)用awaitTermination(通常調(diào)用它后會立即調(diào)用shutdown)來等待ExecutorService到達(dá)終止?fàn)顟B(tài),或者調(diào)用isTerminated來輪詢等待。
27.Timer類處理延遲任務(wù)與周期任務(wù)是有缺陷的,一是它在執(zhí)行所有任務(wù)的時(shí)候只會創(chuàng)建一個(gè)線程,這樣一旦某個(gè)任務(wù)執(zhí)行時(shí)間超過間隔時(shí)間,后續(xù)任務(wù)將會連續(xù)執(zhí)行或被丟棄。另外更嚴(yán)重的是,如果某個(gè)TimeTask拋出了未檢查異常而終止了執(zhí)行線程,那么整個(gè)Timer將被取消。在Java5后,不要再使用Timer。可以用DelayQueue與ScheduledThreadPoolExecutor組合構(gòu)建自己的調(diào)度服務(wù)。
擴(kuò)展:關(guān)于Timer與ScheduleExecutorService執(zhí)行Runnable任務(wù)是否拋出異常對程序的影響差異比較:
28.在Executor框架中,已提交但尚未開始的任務(wù)可以取消,如果是已經(jīng)開始執(zhí)行的任務(wù),只有當(dāng)它們能響應(yīng)中斷時(shí)才可以取消。Future.get如果拋出了異常,會封裝成ExecutionException,可以通過getCause來獲取初始異常。
29.CompletionService將已經(jīng)完成的任務(wù)按照完成順序放置到其內(nèi)置的BlockingQueue隊(duì)列上,每次get取到的都是最新完成的任務(wù)結(jié)果??梢杂肅allable<Void>來表示無返回的任務(wù)。
30.invokeAll按照任務(wù)集合中迭代器的順序?qū)⑺械腇uture添加到返回的集合中。invokeAll會等待所有任務(wù)完成或超時(shí)才返回結(jié)果,不像submit立即異步返回,因?yàn)閕nvokeAll內(nèi)部對FutureList做了循環(huán)get等待。
31.可以使用線程的中斷以及類庫中提供的中斷支持來實(shí)現(xiàn)任務(wù)的可取消特性。每個(gè)線程都有一個(gè)boolean類型的中斷狀態(tài),當(dāng)中斷線程時(shí),此狀態(tài)將設(shè)置為true。關(guān)于線程中斷有三個(gè)方法:
?1)interrupt:線程實(shí)例方法,調(diào)用后中斷實(shí)例線程,設(shè)置該實(shí)例線程的中斷狀態(tài)為true
?2)isInterrupted:線程實(shí)例方法,返回實(shí)例的中斷狀態(tài)ture/false
?3)interrupted:靜態(tài)方法,將清除當(dāng)前線程的中斷狀態(tài),并返回線程之前的狀態(tài)。注意:如果調(diào)用此方法清除當(dāng)前線程的中斷狀態(tài)并返回了true,說明當(dāng)前線程在中斷之前就已經(jīng)是"已中斷"的狀態(tài)了,如果你不做任何處理,那么之前的中斷就被屏蔽掉了,可以通過拋出InterruptedException來響應(yīng)中斷或再次調(diào)用interrupt來恢復(fù)中斷。
一旦一個(gè)線程被終止或正常結(jié)束,都不能再次調(diào)用start方法啟動了,否則會拋出InvalidThreadStateException.
當(dāng)一個(gè)方法由于等待某個(gè)條件變成真而阻塞時(shí),需要提供一種取消機(jī)制。
32.常見的阻塞方法如Thread.sleep,Object.wait在阻塞時(shí)都會檢查線程是否已中斷,如果發(fā)現(xiàn)已中斷,則會先清除中斷狀態(tài),然后拋出InterruptException.(通常,可中斷的方法會在阻塞或進(jìn)行重要的工作前首先檢查中斷,以便能盡快的響應(yīng)中斷)因此,在線程里調(diào)用可拋出InterruptedException異常的阻塞方法時(shí)將使線程處于某種阻塞狀態(tài),如果這個(gè)方法被中斷,那么它將努力提前結(jié)束阻塞狀態(tài)。當(dāng)我們調(diào)用這些阻塞方法時(shí),最好遵循下面的兩種方法之一:1)拋出此InterruptedException異常。?2)如果無法拋出異常,比如在Runnable中運(yùn)行,那么也一定要捕獲此異常,并調(diào)用當(dāng)前線程上的interrupt方法恢復(fù)中斷狀態(tài),使調(diào)用棧中的更高層代碼看到此線程引發(fā)了中斷。?最好不要捕獲異常又不做任何響應(yīng),這樣調(diào)用棧上的高層代碼無法對中斷采取處理措施,因?yàn)榫€程被中斷的證據(jù)已經(jīng)丟失了。
33.在非阻塞的狀態(tài)下,如果調(diào)用線程實(shí)例的interrupt方法,只是設(shè)置了該實(shí)例的中斷狀態(tài),并不會拋出InterruptException,因此如果你的代碼沒有明確觸發(fā)InterruptException的地方,也就意味著該線程實(shí)例沒有很好的響應(yīng)中斷,只是此中斷狀態(tài)將一直保持,直到調(diào)用interrupted明確清除之。
34.對于一些不支持取消但仍會調(diào)用可阻塞的方法操作,必須在循環(huán)中調(diào)用這些阻塞方法,并在發(fā)現(xiàn)中斷后重新嘗試調(diào)用,當(dāng)然當(dāng)這些方法檢測到已中斷會拋出InterruptException,應(yīng)該記錄這個(gè)狀態(tài),并在返回前調(diào)用interrupt恢復(fù)中斷。永遠(yuǎn)不要在方法中調(diào)用"調(diào)用線程(宿主線程)"的interrupt,因?yàn)槟銦o法知道當(dāng)前線程的中斷策略,最好的方式是在方法內(nèi)創(chuàng)建一個(gè)線程,并對它進(jìn)行中斷,因?yàn)槟憧梢钥刂扑闹袛嗖呗浴?/p>
35.當(dāng)Future.get拋出InterruptException或TimeoutException時(shí),如果你知道不再需要結(jié)果了,就可以調(diào)用Future.cancel來取消任務(wù)。
36.對于不可取消中斷的阻塞,如Socket IO, File IO,等待內(nèi)置鎖,可以通過封裝Thread或用newTaskFor,將阻塞方法的不可中斷性轉(zhuǎn)移到其能響應(yīng)的異常上,如通過提供封裝后的cancel方法,將不可中斷的Socket IO讀寫方法在cancel中變?yōu)殛P(guān)閉Socket,這樣read或write將拋出IOException,這樣可以將原本應(yīng)該拋出InterruptException轉(zhuǎn)變成了IOException.
37.對于非正常終止的線程,比如拋出了RuntimeException,如果想做一些清理工作,可以有兩種方式,一是設(shè)置線程的setUncaughtExceptionHandler,通過一個(gè)實(shí)現(xiàn)Thread.UncaughtExceptionHandler接口的類做一些清理,另一個(gè)是在線程啟動時(shí)注冊一個(gè)關(guān)閉鉤子Runtime.getRuntime().addShutdownHook,這樣虛擬機(jī)在關(guān)閉的時(shí)候就會執(zhí)行這些鉤子方法。
38.Executor框架可以將任務(wù)的提交與任務(wù)的執(zhí)行策略解耦開來,但是以下情形卻需要明確指定執(zhí)行策略以保障安全性及避免活躍性問題:
?a)依賴性任務(wù):提交給線程池的任務(wù)需要依賴其他任務(wù),則會隱含的約束執(zhí)行策略
?b)單線程環(huán)境任務(wù):單線程的Executor下執(zhí)行任務(wù)隱含的使用線程封閉機(jī)制保障了線程安全,如果切換到多線程環(huán)境下,會可能導(dǎo)致并發(fā)
?c)對時(shí)間響應(yīng)敏感的任務(wù):如果這些任務(wù)與其他時(shí)間較長的任務(wù)同時(shí)提交給線程池,在單線程及包含少量線程的Executor下會影響敏感任務(wù)的執(zhí)行
?d)使用ThreadLocal的任務(wù):由于線程池會動態(tài)的回收或增加線程,因此“只有當(dāng)線程本地址的生命周期受限于任務(wù)的生命周期時(shí),在線程池中的線程使用ThreadLocal才有意義”,而且不應(yīng)該使用ThreadLocal在任務(wù)之間傳遞值。
39.只要線程池中的任務(wù)需要無限期的等待一些必須由池中其他任務(wù)才能提供的資源或條件,除非線程池足夠大,否則將發(fā)生饑餓死鎖.因此線程池中最好運(yùn)行那些同類型并且相互獨(dú)立的任務(wù),以使線程池達(dá)到最大性能。如果確實(shí)需要執(zhí)行不同類型的任務(wù),應(yīng)該考慮使用多個(gè)線程池。
40.線程池設(shè)置大小公式:Nthread =?Ncpu?* Ucpu(cpu目標(biāo)利用率) * (1+W/C(等待時(shí)間與計(jì)算時(shí)間比))。對于計(jì)算密集型任務(wù),在擁有N個(gè)處理器的系統(tǒng)上,當(dāng)線程池的大小為N+1時(shí),通常能實(shí)現(xiàn)最優(yōu)利用率。如果是其他資源限制,那么用該資源的總量除以每個(gè)任務(wù)對該資源的需求量,所得結(jié)果就是線程池大小的上限。
Amdahl定律:
并發(fā)后的加速比?<=1/(F+(1-F)/N)
其中F為串行計(jì)算部分的百分比,N為CPU數(shù)
41.線程池的基本大小即沒有任務(wù)執(zhí)行時(shí)線程池的大小,只有在工作隊(duì)列已滿才會創(chuàng)建超出這個(gè)數(shù)量的線程。如果某個(gè)線程超過了存活時(shí)間,該線程被標(biāo)記為可回收,如果同時(shí)當(dāng)前線程池大小超過基本大小,該線程將被終止。
42.對于沒有使用SynchronousQueue作為工作隊(duì)列的線程池(newCacheThreadPool默認(rèn)使用該隊(duì)列),如果線程池中的線程數(shù)量等于基本大小,僅當(dāng)隊(duì)列已滿時(shí)才會創(chuàng)建新的線程,因此如果設(shè)置基本大小為0且隊(duì)列未滿,任務(wù)達(dá)到后先進(jìn)入隊(duì)列,由于此時(shí)線程數(shù)為0因此不會執(zhí)行任務(wù),只有待隊(duì)列滿時(shí)才會真正執(zhí)行任務(wù)。
43.基本任務(wù)排隊(duì)方法及被何種線程池采用:
?a:無界隊(duì)列:如無界LinkedBlockingQueue(FIFO),newFixedThreadPool和newSingleThreadExecutor默認(rèn)使用此隊(duì)列,此隊(duì)列的好處是能讓所有線程池中的線程保持忙碌狀態(tài),缺點(diǎn)是一旦生產(chǎn)大于消費(fèi),隊(duì)列無限制增大耗盡內(nèi)存。
?b:有界隊(duì)列:如ArrayBlockingQueue(FIFO),有界的LinkedBlockingQueue(FIFO),PriorityBlockingQueue(任務(wù)按自然順序或?qū)崿F(xiàn)Comparable排序)。當(dāng)有界隊(duì)列滿后會根據(jù)飽和策略處理.
?c:同步移交(Synchronous Handoff):SynchronousQueue,必須有空閑線程(或還能創(chuàng)建新線程)等待接受時(shí)才可以,否則拒絕。一般只用在線程池?zé)o界或可以拒絕任務(wù)時(shí),如在newCachedThreadPool中(由于運(yùn)用了此隊(duì)列,因此它能比固定大小的線程池提供更好的排隊(duì)性能,特別是Java6優(yōu)化了非阻塞算法,因此只要不是受特殊資源限制,都建議用newCachedThreadPool作為默認(rèn)的選擇).
44.四種飽和策略(ThreadPoolExecutor可以調(diào)用setRejectedExecutionHandler來設(shè)置):
?a)中止策略:默認(rèn)策略,拋出未檢查的RejectedExecutionException.
?b)拋棄策略:放棄該新任務(wù)
?c)拋棄最舊的策略:根據(jù)FIFO,最舊的就是下一個(gè)將被執(zhí)行的任務(wù)被拋棄以嘗試提交新的任務(wù)。因此一般該策略不和FIFO一起用。
?d)調(diào)用者運(yùn)行策略:即在調(diào)用了execute的主線程中執(zhí)行,這樣在一段時(shí)間內(nèi)無暇再接受新的任務(wù),新到達(dá)的請求停留在TCP層,如果持續(xù)過載,TCP層也會拋棄請求的,從而實(shí)現(xiàn)一種平緩的性能降低。其過程為:線程池--->工作隊(duì)列---->應(yīng)用程序--->TCP層--->客戶端。
45.可以通過實(shí)現(xiàn)ThreadFactory接口以及繼承Thread類來自己定義如何產(chǎn)生新線程.比如可以加入日志,調(diào)用setUncaughExceptionHandler來設(shè)置該線程由于未捕獲到異常而突然終止時(shí)調(diào)用的處理程序。
46.當(dāng)然也可以調(diào)用Executors中的unconfigurableExecutorService封裝一個(gè)具體的ExecutorService以隱藏對ThreadPoolExecutor的配置。另外如果繼承ThreadPoolExecutor,可以更靈活的擴(kuò)張線程池,主要有以下幾個(gè)方法:
?a)beforeExecute:任務(wù)執(zhí)行前調(diào)用,如果拋出異常,則任務(wù)不被執(zhí)行,此任務(wù)結(jié)束
?b)afterExecute:只要任務(wù)在完成后不是帶有一個(gè)Error,不管是正常返回還是拋出一個(gè)異常都會被執(zhí)行。
?c)terminated:所有任務(wù)已經(jīng)完成且所有工作者線程關(guān)閉后調(diào)用,可以用來釋放期生命期分配的資源及發(fā)送通知,記錄日志,收集統(tǒng)計(jì)信息等。
47.多線程很重要的一個(gè)應(yīng)用是將“多個(gè)迭代之間彼此獨(dú)立,而且每個(gè)迭代操作執(zhí)行的工作量比管理一個(gè)新任務(wù)開銷大的”串行計(jì)算轉(zhuǎn)換為并行,多個(gè)線程同時(shí)獨(dú)立計(jì)算各部分結(jié)果,當(dāng)某一個(gè)線程得到結(jié)果時(shí),通過設(shè)置一個(gè)公共同步變量或synchronized方法來通知不需再產(chǎn)生新的任務(wù)并可以通知其他任務(wù)線程適當(dāng)結(jié)束自己。當(dāng)然,也要考慮實(shí)在沒有結(jié)果的情況,為避免永遠(yuǎn)等待結(jié)果,可以設(shè)置一個(gè)同步計(jì)數(shù)器,每個(gè)處理任務(wù)結(jié)束時(shí)在finally塊先將計(jì)數(shù)器減一并查看當(dāng)前剩余工作線程是否為0,為0則表示無結(jié)果,可以設(shè)置一個(gè)空結(jié)果。
并發(fā)的不良后果-活躍性故障
48.活躍性故障最常見的是鎖順序死鎖,即兩個(gè)線程試圖以不同的順序來獲得相同的鎖,或者多個(gè)線程相互持有彼此正在等待的鎖而又不釋放自己已持有的鎖時(shí)會發(fā)生死鎖情況。包括以下幾種情況;1:不同方法中鎖的順序不一樣,A方法先鎖Key1,再鎖Key2,而B方法先鎖Key2,再鎖Key1. 2:方法中對傳入的參數(shù)加鎖,如果兩個(gè)參數(shù)類型相同,當(dāng)不同地方的調(diào)用這個(gè)方法傳入的參數(shù)順序相反,如fromAccount,toAccount 3:協(xié)作對象間加鎖方法相互調(diào)用出現(xiàn)隱秘的死鎖。解決死鎖有以下幾種方法;1:通過對象的hash值判斷鎖順序從而保證鎖順序一致?2:增加加時(shí)鎖?3:減小同步加鎖的代碼塊,將方法級加鎖改為代碼塊級加鎖,盡量通過良好的線程封裝開放調(diào)用。4.使用支持定時(shí)的鎖??赏ㄟ^死鎖時(shí)轉(zhuǎn)儲的信息來分析死鎖發(fā)生的原因。
49.活躍性故障另外兩個(gè)不良后果,一個(gè)是線程饑餓,即由于線程優(yōu)先級的調(diào)整,導(dǎo)致有的線程始終無法得到cpu執(zhí)行,另一個(gè)是活鎖,即多個(gè)相互協(xié)作的線程都對彼此進(jìn)行響應(yīng)從而修改各自的狀態(tài),并使得任何一方都無法繼續(xù)。比如過度的錯誤恢復(fù)代碼。一般在并發(fā)應(yīng)用中,通過隨機(jī)等待長度的時(shí)間和回退可以避免之。
并發(fā)的不良后果-線程調(diào)度開銷
50.線程引入主要有以下開銷:
?a:線程之間的協(xié)調(diào)(加鎖,觸發(fā)信號,內(nèi)存同步)
?b:上下文切換(JVM和操作系統(tǒng)間,新開線程的數(shù)據(jù)結(jié)構(gòu)加入緩存,阻塞線程被調(diào)度)
?c:內(nèi)存同步(如synchronized和volatile提供的可見性保證中會使用內(nèi)存柵欄刷新緩存,使緩存無效,刷新硬件的寫緩沖以及停止執(zhí)行管道)
?d:阻塞(競爭失敗的鎖無論是自旋等待還是被操作系統(tǒng)掛起)
51.對并發(fā)程序的測試包含安全性(正確性,)及活躍性(吞吐量,響應(yīng)性,可伸縮性)。測試中盡量讓每個(gè)計(jì)算結(jié)果都被使用到,并確保其值不可預(yù)測,哪怕是與nanoTime任意的比較,因?yàn)橐粋€(gè)智能編譯器將用預(yù)先計(jì)算的結(jié)果來代替計(jì)算過程。
52.對阻塞性的測試類似異常測試,如果代碼執(zhí)行到了阻塞方法的下面,說明測試失敗。(當(dāng)然也要注意測試時(shí)有方法解除阻塞)。對并發(fā)的測試,線程數(shù)應(yīng)該大于CPU數(shù),如果要測試并發(fā)時(shí)存入取出元素的順序,可以利用元素hash和nanoTime加一些"移位"的方式求和比較。利用CyclicBarrier和CountDownLatch控制線程同步執(zhí)行。
53.垃圾回收,java動態(tài)編譯被多次執(zhí)行的代碼為機(jī)器碼,對代碼路徑不真實(shí)采樣,不真實(shí)的競爭程度(測試時(shí)計(jì)算過多或過少)都會影響到測試代碼的效率。
54.內(nèi)置鎖無法在等待時(shí)中斷,且必須在獲取該鎖的代碼塊中釋放。Lock提供了一種無條件的,可輪詢的,定時(shí)的,可中斷的鎖獲取操作。必須記住在finally塊中調(diào)用釋放鎖的操作。lockInterruptibly方法能夠在獲得鎖的同時(shí)保持對中斷的響應(yīng)。java6開始內(nèi)置鎖的性能已經(jīng)與ReentranLock相當(dāng)了,但是ReentrantLock是非塊結(jié)構(gòu)的,獲取鎖的操作不能與特定的棧幀關(guān)聯(lián)。
55.當(dāng)持有鎖的時(shí)間相對較長,或者請求鎖的平均時(shí)間間隔較長,可以使用公平鎖,即當(dāng)鎖不可用時(shí),新請求的線程將被放在隊(duì)列里而不是一有可用鎖立馬插隊(duì)。
56.讀寫鎖的分離可以提高并發(fā),有一些選項(xiàng)需要注意:釋放優(yōu)先,讀線程插隊(duì),重入性,寫降讀,讀升寫。
57.可以用內(nèi)置的條件隊(duì)列,顯式的Condition對象,以及AbstractQueueSynchronizer框架來構(gòu)造自己的同步器。
58.Object.wait會自動釋放鎖,并請求操作系統(tǒng)掛起當(dāng)前線程。在喚醒線程后,wait在返回前還要重新獲取鎖。由于多個(gè)線程可能基于不同的條件(非空,已滿)在同一條件對象上等待,因此如果調(diào)用notify可能出現(xiàn)通知失誤,如已經(jīng)非空,但是notify通知卻喚醒了正在等待已滿狀態(tài)的wait。一般來說我們應(yīng)該用notifyAll,除非同時(shí)滿足以下兩個(gè)條件1.所有等待線程的等待類型相同,2.單進(jìn)單出(在條件變量上的每次通知,最多只能喚醒一個(gè)線程)
59.內(nèi)置的條件隊(duì)列相關(guān)者Object中的wait,notify,notifyAll對于synchronized內(nèi)置鎖的關(guān)系,正如Condition中的await,signal,signalAll對于顯式的Condition,它們是一一配對使用的,因?yàn)楣芾頎顟B(tài)依賴性的機(jī)制必須與確保狀態(tài)一致性的機(jī)制關(guān)聯(lián)。只不過后者提供了更多的控制,包括每個(gè)鎖對應(yīng)于多個(gè)等待線程集,可中斷或不可中斷的條件等待,公平或非公平的隊(duì)列操作以及基于時(shí)限的等待。所有的阻塞等待都是在獲取到鎖的前提下進(jìn)行的,調(diào)用wait或await時(shí)釋放已經(jīng)得到的鎖并開始等待,當(dāng)notify(notifyAll)或signal(signalAll)后被選擇執(zhí)行的線程開始重新獲取鎖,并從等待處往下執(zhí)行。
60.AQS是一個(gè)構(gòu)建鎖和同步器的框架,其最基本的操作包括各種形式的獲取操作和釋放操作,其內(nèi)用一個(gè)整數(shù)來表示狀態(tài)信息,不同的同步實(shí)現(xiàn)無非用此整數(shù)外加一些輔助額外狀態(tài)變量來表示,如ReentrantLock用它來表示所有者線程已經(jīng)重復(fù)獲取該鎖的次數(shù),Semaphore用它來表示剩余的許可數(shù)量,F(xiàn)utureTask用它來表示任務(wù)的狀態(tài),所有的具體同步實(shí)現(xiàn)都沒有直接擴(kuò)展AQS,而是將它們相應(yīng)的功能委托給私有的AQS子類。
AQS還在內(nèi)部維護(hù)一個(gè)等待線程隊(duì)列,記錄某個(gè)線程請求的是獨(dú)占訪問還是共享訪問。
61.ReentrantReadWriteLock使用一個(gè)16位的狀態(tài)來表示寫入鎖計(jì)數(shù),并使用獨(dú)占的獲取與釋放方法,并使用另一個(gè)16位的狀態(tài)來表示讀取的計(jì)數(shù),并用共享的獲取與釋放方法。因此當(dāng)鎖可用時(shí),如果位于隊(duì)列頭部的線程執(zhí)行寫入操作,那么線程會獲得這個(gè)鎖,如果位于隊(duì)列的頭部執(zhí)行讀取線程,那么隊(duì)列中的第一個(gè)寫入線程之前所有的讀線程都將獲得此讀取鎖。(讀取優(yōu)先策略)
62.CAS的典型使用模式:首先從V中讀取值A(chǔ),并根據(jù)A計(jì)算新值B,然后再通過CAS以原子方式將V中的值由A變成B,其主要缺點(diǎn)是使調(diào)用者處理競爭問題(通過重試,回退,放棄),而在鎖中能自動處理競爭問題(線程在獲得鎖之前將一直阻塞)。
63.java的原子變量類AtomicXXX使用底層的CAS平臺指令為數(shù)字類型及引用類型提供一種高效的操作,java.util.concurrent包中的大多數(shù)類在實(shí)現(xiàn)時(shí)則直接或間接地使用了這些原子類。
64.創(chuàng)建非阻塞的算法關(guān)鍵在于找出將原子修改的范圍縮小到單個(gè)變量上,同時(shí)還要維護(hù)數(shù)據(jù)的一致性。