在Java並發編程中,對於線程安全是非常重要的,也是必須要考慮的一個問題.可以這麼說,只要涉及到網絡的,都必須考慮線程安全問題.好了,開始噼裡啪啦地開始敲代碼之前,我覺得有必要了解一些文绉绉的理論知識,因為這些理論知識是我們敲出來的代碼是否是線程安全的一個依據.
當多個線程訪問某個狀態變量並且其中有一個線程執行寫入操作的時候,必須考慮采用同步機制來協同這些線程對變量的訪問,Java中的主要同步機制是關鍵字synchronized,它提供了一種獨占的加鎖方式,但"同步"這個術語還包括類型的變量,顯式鎖(Explicit)以及原子變量.
如果當多個線程訪問同一個可變狀態變量時,沒有使用合適的同步,那麼就會出現錯誤.有三種方式可以修復這個問題:
1)不在線程間共享該狀態變量
2)將狀態變量修改為不可變的變量
3)在訪問狀態變量時使用同步
當設計線程安全的類時,良好的面向對象技術,不可修改性,以及明晰的不變性規范都能起到一定的幫助作用.
當多個線程訪問某個類時,不管運行時環境采用何種調用方式,或者這些線程將如何交替執行,並且在主調代碼中不需要任務額外的同步或協同,這個類都能表現出正確的行為,那麼就稱這個類是線程安全的.
這裡的正確性是指某個類的行為與其規范安全一致,也就是說這個類是實現什麼功能的就正確的實現了什麼功能.
在線程安全的類中,封裝了必要的同步機制,所以客戶端無需進一步采取同步措施.
前面提到了無狀態對象,無狀態對象既不包含任何域,也不包含任何其他類中域的引用,無狀態對象一定是線程安全的.大多數Servlet都是無狀態的,從而極大的降低了在實現Servlet線程安全性的復雜性,只有當Servlet在處理請求時需要保存一些信息,線程才會成為一個安全的線程.
原子性
當某個計算正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件.所謂的競態條件就是多個線程執行的順序不同,那麼執行的結果就會有差異,必須是一個正確的執行嗯順序才會得出正確的結果.
有時候為了保證原子性,會采用一種措施,就是延遲初始化,將對象初始化操作推遲到實際使用時才進行,同時要確保只被初始化一次.
java.util.concurren.atomic包中包含了一些原子變量類,用於實現在數值和對象引用撒謊嗯的原子狀態轉換.
在實際情況中,應盡可能地使用現有的線程安全對象(例如AcomicLong )來管理類的狀態,與非線程安全的對象相比.判斷線程安全對象德 可能狀態,及其狀態轉換情況要更為容易,從而也更容易維護和驗證線程安全性.
加鎖機制
要保持狀態的一致性,就需要在單個原子操作中更新所相關的狀態變量.那麼如果是多個原子操作組成一個原子操作呢?那就用到加鎖機制了.
1.內置鎖
同步代碼塊:包括兩個部分,一個作為鎖的對象引用,一個最為由這個鎖保護的對象的代碼塊.
synchronized (lock){ //訪問或修改由鎖保護的共享狀態 }
每個Java對象都可以用做一個實現同步鎖。這些鎖稱為內置鎖(Intrinsic Lock)或監視器鎖(Monitor Lock)
線程在進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時自動釋放鎖,而無論是聽過控制路徑退出,還是通過同步代碼塊中執行拋出異常退出。獲得內置鎖的唯一途徑就是進入這個由鎖保護的同步代碼塊或方法。
Java內置鎖相當於一種互斥體(或互斥鎖),這就意味著最多只有一個線程能持有這種鎖。
由於每次只能有一個線程執行內置鎖的代碼塊,因此, 由這個鎖保護的同步代碼塊會議原子方式執行,多個線在執行該代碼塊時也不會相互 干擾,並發環境中的原子性與事務應用程序有著相同的含義,一組語句作為一個不可分割的單元被執行。任何一個執行同步代碼塊的線程,都不可能看到有其他線程正在執行由同一個鎖保護的同步代碼塊。
2.重入
“重入”獲取鎖的操作的粒度是“線程”,而不是“調用”。
重入是一種實現方法是為每一個鎖關聯一個獲取計數值和一個所有者線程,當計數值為0時,這個鎖就被認為是沒有任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且獲取計數值置為1.如果同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數器會相應地遞減,計數值為0時,這個鎖將被釋放。
重入進一步提升了加鎖行為的封裝性,因此,簡化了面向並發代碼的開發
用鎖來保護狀態
對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖,在這種情況下,我們稱狀態變量由這個鎖保護。
對象的內置鎖與其狀態之間沒有內在的關聯,之所以每個對象都有一個內置鎖,只是為了免去顯式創建對象。
一種常見的加鎖約定是,將所有的可變狀態都封裝在對象內部,並通過對象的內置鎖對所有訪問可變狀態的代碼路徑進同步,使得在該對象上不會發生並發訪問。
別費所有數據都需要鎖的保護,只有被多個線程同步訪問的可變數據才需要通過所來保護。
對於每個包含多個變量的不變性條件,其中涉及的所有變量都需要由同一個鎖保護。
活躍性與性能
要確保同步代碼塊不要過小,並且不要將本應該是原子的操作拆分到多個同步代碼塊中,應該盡量將不影響共享狀態且執行時間較長的操作從同步代碼塊中分離出去,從而在這些操作的執行過程中其他線程可以訪問共享狀態。
當執行時間較長的計算或者可能無法快速完成的操作(如網絡I/O或控制台I/O),一定不要只有鎖。