66,同步訪問共享的可變數(shù)據(jù)
關(guān)鍵字synchronized可以保證在同一時刻,只有一個線程可以執(zhí)行某一個方法,或者某一個代碼塊。
同步不僅可以阻止一個線程看到對象處于不一致的狀態(tài)中,它還可以保證進(jìn)入同步方法或者同步代碼塊的每個線程,都看到同一個鎖保護(hù)的之前的修改效果。
Volatile 變量可用于提供線程安全,但是只能應(yīng)用于非常有限的一組用例:多個變量之間或者某個變量的當(dāng)前值與修改后值之間沒有約束。對變量的寫操作不依賴于當(dāng)前值,該變量沒有包含在具有其他變量的不變式中。不具有原子性。
將可變數(shù)據(jù)控制在單個線程中,不共享可變數(shù)據(jù)。當(dāng)多個線程共享可變數(shù)據(jù)的時候,每個讀或?qū)憯?shù)據(jù)的線程都必須執(zhí)行同步。
但一個線程短時間內(nèi)修改一個數(shù)據(jù)對象,然后與其他線程共享,這是尅接受的,只同步共享對象引用的動作。其他線程只是讀取沒有修改,這種對象事實上是不可變的,將這種對象從一個線程傳遞到其他線程被稱作安全發(fā)布。
安全發(fā)布對象方法:可以將它保存在靜態(tài)域中,作為類初始化的一部分;可以將它保存在volatile域,final域或者通過正常鎖定訪問的域中;或者將它放到并發(fā)集合中。
67,避免過度同步
依據(jù)情況不同,過度同步可能會導(dǎo)致性能降低,死鎖,甚至不確定的行為。
為了避免活性失敗和安全性失敗,在一個被同步的方法或代碼塊中,永遠(yuǎn)不要放棄對客戶端的控制。換句話說,在一個被同步的區(qū)域內(nèi)部,不要調(diào)用設(shè)計成要被覆蓋的方法,或者由客戶端以函數(shù)對象的形式提供的方法。
在同步區(qū)域調(diào)用外來方法被稱作“開放調(diào)用”。除了可以避免死鎖之外,開放調(diào)用還可以極大地增加并發(fā)性。
通常,你應(yīng)該在同步區(qū)域內(nèi)做盡量少的工作。
在這個多核的時代多度同步的實際成本并不是指獲得鎖所花費的CPu時間;而是指失去了并行地機會,以及因為需要確保每個核都有一個一致的內(nèi)存視圖而導(dǎo)致的延遲。過度同步的另一項潛在開銷在于,他會限制Vm優(yōu)化代碼的能力。
StringBuffer實例幾乎總是被用于單個線程中,而它們執(zhí)行的卻是內(nèi)部同步。為此,StringBuffer基本都由StringBuilder代替。
如果一個可變類要并發(fā)使用,應(yīng)該使這個類編程線程安全的,通過內(nèi)部同步,你還可以獲得,明顯比外部鎖定整個對象更高的并發(fā)性。否則,就不要在內(nèi)部同步。讓客戶在必要的時候從外部同步。
68,executor和task優(yōu)先于線程
- 如果編寫的是小程序,或者輕載的服務(wù)器,使用Executor.newCachedThreadPool通常是個不錯的選擇。
- 在大負(fù)載的服務(wù)器中,最好使用Executor.newFixedThreadPool,它為你提供了一個包含固定線程數(shù)目的線程池,或者為了最大限度的控制它,就直接使用ThreadPoolExecutor類。
你不僅應(yīng)該盡量不要編寫自己的工作隊列,而且還應(yīng)該盡量不直接使用線程。現(xiàn)在的關(guān)鍵抽象不再是Thread了,它以前既充當(dāng)工作單位,又是執(zhí)行機制。工作單位和工作單位是分開的,現(xiàn)在的關(guān)鍵抽象是工作單元,稱作任務(wù)(task)。
任務(wù)有兩種:Runnable及其近親Callable(它與Runnable類似,但它會返回值)。執(zhí)行任務(wù)的通用機制是executor service。
Executor FrameWork也有一個可以代替java.util.Timer的東西,即ScheduledThreadPoolExecutor。
Timer只有一個線程來執(zhí)行任務(wù),如果timer唯一的線程拋出未被捕捉的異常,timer就會停止工作。而線程池executor支持多個線程,并且優(yōu)雅的從拋出未受檢異常的任務(wù)中恢復(fù)。
69,并發(fā)工具優(yōu)先于wait和notify
java.util.concurrent中更高級的工具分三類:Executor Framework,并發(fā)集合(Concurrent Collection)以及同步器(Synchronizer)。
并發(fā)集合為標(biāo)準(zhǔn)的集合接口提供了高性能的并發(fā)實現(xiàn),這些實現(xiàn)在內(nèi)部自己管理同步。因此,并發(fā)集合中不可能排除并發(fā)活動;將它鎖定沒有什么作用,只會使程序的速度變慢。
70,線程安全性的文檔化
如果你沒有在一個類的文檔中描述其行為的并發(fā)情況,使用這個類的程序員將不得不做出某些假設(shè)。如果這些假設(shè)是錯誤的,這樣得到的程序就可能缺少足夠的同步,或者過度同步。無論屬于哪種情況,都可能會發(fā)生嚴(yán)重的錯誤。
一個類為了可被多個線程安全使用,必須在文檔中清楚地說明它所支持的線程安全性級別。
- 不可變的——這個類的實例是不可變的。這樣的例子包括String,Long,BigInteger。
- 無條件的線程安全——這個類的實例是可變的,但是這個類有足夠的內(nèi)部同步。例子包括Random,ConconcurrentHashMap。
- 有條件的線程安全——除了有些方法為進(jìn)行安全的并發(fā)使用而需要外部同步之外,這種線程安全級別與無條件安全相同。例子包括:Collections.synhronized包裝返回的集合,它們的迭代器要求外部同步。
- 非線程安全——這個類的實例是可變的。為了并發(fā)使用它們,客戶必須利用自己選擇的外部同步包圍每個方法調(diào)用。例子包括ArrayList
- 線程對立的——這個類不能安全地被多個線程并發(fā)使用,即使所有的方法調(diào)用都被外圍同步包圍。
類的線程安全說明通常放在它的文檔中,但帶有特殊線程安全屬性的方法則應(yīng)該在它們自己的文檔注釋中說明它們的屬性。
私有鎖只能用在無條件的線程安全類上。私有鎖對象模式特別適用于那些專門為繼承而設(shè)計的類。如果這種類適用它的實例作為鎖的對象,子類可能很容易在無意中妨礙基類的操作,反之亦然。
61,慎用延遲初始化
延遲初始化是延遲到需要域的值時才將它初始化的這種行為。
對于延遲初始化,最好建議“除非絕對必要,否則就不要那么做”。延遲化降低了初始化類或者創(chuàng)建實例的開銷,卻增加了訪問被延遲初始化的域的開銷。
如果域只是在類的實例部分被訪問,并且初始化這個域的開銷很高,可能就值得進(jìn)行延遲初始化。
- 如果出于性能的考慮而需要對靜態(tài)域使用延遲初始化,就使用lazy initialization holder class 模式。保證在被用時初始化。
- 如果出于性能的考慮而需要對實例域使用延遲初始化,就使用雙重檢查模式。這種模式避免了在域被初始化之后訪問這個域時的鎖定開銷。
第一次檢查時沒有鎖定,看看這個域是否被初始化;第二次檢查時又鎖定。只有當(dāng)?shù)诙螜z查時標(biāo)明這個域沒有被初始化,才進(jìn)行初始化。如果域已經(jīng)被初始化就不會有鎖定,域被聲明為volatile很重要。
72,不要依賴于線程調(diào)度器
當(dāng)有多個線程可以運行時,由線程調(diào)度器決定哪些線程將會運行,以及運行多長時間。
任何依賴于線程調(diào)度器來達(dá)到正確性或者性能要求的程序,很有可能都是不可移植的。
要編寫健壯,響應(yīng)良好的,可移植的多線程應(yīng)用程序,最好的辦法是確保可運行線程的平均數(shù)量不明顯多于處理器的數(shù)量。
線程優(yōu)先級是java平臺上最不可移植的特征了。
73,避免使用線程組
線程組并沒有提供太多有用的功能,而且他們提供的許多功能還都有缺陷的。
如果你正在設(shè)計的一個類需要處理線程的邏輯組,或許就應(yīng)該使用線程池executor。