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