什麼是線程安全性
安全性實際上接近於“所見即所知”的狀態,就是在單線程和多線程中都不會被開發者不可見的因素改變其狀態。
在線程安全類中封裝了必要的同步機制,因此客戶端不需要進一步採取同步措施。
如果一個類沒有攜帶任何屬性,那麼它一定是線程安全的。大多數的Servlet都是線程安全的。
狀態,說的明顯點就是對象的屬性。屬性的變化只能依靠類中能改變這些屬性的方法。
原子性
原子操作,就是在執行某個代碼或代碼快時,對其所持有的變量,其它的線程無法訪問並修改。換句話說,原子操作的代碼或代碼塊,同一時間只能在一個線程中執行。
競態條件
在併發線程中,由於不恰當的執行時序導致不正確的結果,這種現象成爲競態條件。常見的靜態條件例子就是count++,看似是一個原子操作,事實上它包含了三個狀態,讀取--改變--寫入,併發線程時,如果在一個線程在未執行寫入前另一個線程執行讀取,這就是競態條件。不存在競態條件的操作可以認爲是原子操作。
競態條件最典型的模型是“先檢查,後執行”,首先,你在檢查某個狀態A,假設A爲真時,你去執行一個操作,操作完成後得到某個結果,現在問題是,你可能在執行操作過程中,A已經變成了假,因此得到的結果反而會帶來不可預料的錯誤。
單例模式中的延遲初始化(對象在被使用時才被初始化,且只被初始化一次)會導致競態條件。比如如下代碼:
複合操作
多個原子操作的組合操作便構成了一個複合操作,複合操作容易帶來安全性問題,所以在count++可以替換爲原子操作的AtomicLong.incrementAndGet(1)。在java.util.concurrent.atomic包中封裝了一些原子變量類,用來執行原子操作。
加鎖機制
內置鎖
內置鎖(監視鎖)是互斥鎖的一種,通過持有某個對象(或者類)來使其保護的代碼快達到原子操作的目的。值得注意的是,內置鎖只能鎖代碼塊,不能鎖屬性和變量。而且鎖旗標一定是一個對象。
被加鎖的代碼塊,只要執行完畢就會釋放鎖旗標,不管是正常執行完畢還是拋出異常中斷執行。
鎖可以由一個對象充當,也可以由一個類的所有對象充當,如下演示了不同鎖旗標的內置鎖的實現。
重入
簡單的說,重入的意思就是持有鎖的東西是線程,而非某個類的某個對象。重入的好處就是一個線程請求自己已經獲得的鎖旗標時,這個請求會成功。重入有點類似與多重加鎖,但是鎖的鑰匙只有一個。實現重入的一種方法是,爲鎖關聯一個計數器,當計數器爲0時,說明鎖旗標未被任何線程佔據,此時才能被線程請求。當被同一線程重入時,每重入一次,計數器加一,每釋放一次,計數器減一。 減到0後,釋放鎖旗標。其他線程又能請求。
以下例子將演示在重寫父類同步方法時的重入的重要性。
線程執行子類的method時,獲得this的鎖旗標,在執行super.method時,依然請求this的鎖旗標,如果沒有重入,那麼this的鎖旗標一直被佔據,spuer.method一直無法獲得鎖旗標,而Son的method方法永遠不會執行完畢,那麼鎖一直不會釋放,這樣就一直死鎖。
鎖來保護狀態
如果需要鎖來保護某個對象的狀態,那麼在每個可能使用到該對象的地方,都需要加鎖。很顯然這不實際。顯式聲明鎖的方法並不適合大型程序編寫。加鎖的恰當方式是,在對象所在的類中就對操作其狀態的方法進行加鎖。在擴展這個類時,記得在擴展方法上加鎖,否則很容易破壞類的加鎖協議。
對於一個對象來說,每個共享和可變的變量都應該由一種鎖來保護。
安全對象的操作可以說是安全的,但是多個安全對象的操作合在一起就不是線程安全的了。
最後補充一下,當執行耗時操作(IO)時,千萬不要持有鎖,否則會出現糟糕的體驗。