在 Java 中,鎖好像是顆萬能藥,沒什麼問題是加鎖解決不了的。的確,鎖能解決絕大部分的併發問題。編程
然而,最簡單的東西也每每最容易出現問題。你只要稍有不慎,不但 Bug 沒有解決,還得花費大量的時間作各類排查。segmentfault
既然如此,咱們就來好好看看:爲何用了鎖,程序仍是出錯了。併發
一說到鎖,你的大腦中可能立馬想起這個模型。性能
中間藍色的一段代碼,叫作:臨界區。線程進入臨界區前,先嚐試加鎖,若是成功,就進入臨界區。此時,這個線程持有鎖,等執行完臨界區的代碼後,持有鎖的線程就會執行解鎖。this
一樣的道理,若是加鎖失敗,線程就會一直等待,直到持有鎖的線程解鎖後,再從新嘗試加鎖。spa
這是鎖的簡易模型,看起來簡單明瞭,但這個模型是錯的,它忽略了最最重要的一點:鎖與資源的關係。線程
你還記得嗎?鎖是爲了實現互斥,即:在同一時刻,一個資源只能由一個線程操做。3d
換句話說,之因此要用鎖,就是要保護某些資源。所以,鎖與資源之間的關係,是重點中的重點。不少併發 Bug 就是忽略了這一點致使的。好比,你看下面這段代碼:code
class Account { private int balance; // 轉帳 synchronized void transfer(Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
你可能會以爲,這段代碼沒問題呀?transfer()
方法不是加鎖了嗎?但在併發環境下,轉帳後,餘額極可能對不上。緣由也很簡單,沒考慮鎖和資源的關係。對象
既然如此,那正確的鎖模型是怎樣的呢?
首先,咱們要標註出受保護的資源 R;而後,爲資源 R 建立一把鎖 LR;最後,在進出臨界區的時候,再加上加鎖、解鎖操做。
其中,鎖 LR
和受保護資源 R
之間有一條關聯線,這很是重要,表明的是:鎖與資源之間,是有對應關係的。
打個比方,你家的鎖只能保護你家的東西,我家的鎖只能保護我家的東西。這個關係要搞清楚,否則,就是鎖自家的門來保護他家的東西。
能夠說,咱們平時對鎖的理解都是錯的。雖然這通常不會有什麼問題,但只要出現問題,就是公司破產之類的大事。
好比,我前東家的支付系統,就是由於鎖的問題,鉅虧了幾十萬,負責人引咎辭職。在瀕臨倒閉之際,我接手了,這個項目才起死回生。
因此,若是你不想重蹈覆轍,而是想升職加薪。那必需要掌握正確的鎖模型,千萬不能忽略鎖和資源的關係。
你已經知道了,想要用好用鎖,關鍵是搞清楚鎖和資源的關係。那麼,這二者的關係是怎樣的呢?
資源和鎖之間的關係是 N:1。
換句話說,你能夠用一把鎖,來保護多個資源。可是,你不能用多把鎖,來保護一個資源。緣由在於,若是你針對一個資源建立了多把鎖,那麼就達不到互斥的效果了。
打個比方,現實世界中,你能夠用好幾把鎖,來保護你家的東西。但在編程世界中,你不能這樣作。你看這個例子:
class SafeCalc { private long value = 0L; void addOne() { synchronized (this) { value += 1; } } void addTwo() { synchronized (SafeCalc.class) { value += 2; } } }
你看下 addOne()
和 addTwo()
兩個方法,它們分別用了兩把鎖 this
和 SafeCalc.class
。雖然都是保護同一個資源,但臨界區被拆成了 2 個,而這 2 個臨界區是沒有互斥關係的,這致使了併發問題。
那問題該怎麼解決呢?
很簡單,不要用多把鎖來保護一個資源,你能夠只用 this
,又或者只用 SafeCalc.class
。
固然,這個例子比較簡單。在現實工做中,狀況確定更加複雜,咱們要保護的資源不止一個,而是多個,這又該怎麼處理呢?
不管是單個資源,仍是多個資源,關鍵都在於:鎖要覆蓋全部受保護的資源。
單個資源比較好理解,但若是要保護的資源不止一個,那咱們首先要作的是,區分這些資源是否有關聯,這分爲兩種狀況。
第一,如何保護沒有關聯的多個資源,這和處理單個資源差很少。若是不考慮性能,你能夠用一把鎖來保護;若是想提升性能,你能夠用不一樣的鎖來保護。
你看下面的代碼:
class Account { // 餘額 private Integer balance; // 密碼 private String password; // 鎖:保護餘額 private final Object balLock = new Object(); // 鎖:保護密碼 private final Object pwLock = new Object(); // 取款 void withdraw(Integer amt) { synchronized (balLock) { if (this.balance > amt) { this.balance -= amt; } } } // 更改密碼 void updatePassword(String pw) { synchronized (pwLock) { this.password = pw; } } }
帳戶類-Account
有兩個方法:取款-withdraw()
、更改密碼-updatePassword()
。從功能上看,這兩個方法沒什麼關聯。因此,爲了提高性能,我用了兩把鎖,讓它們各管各的。
固然,你也能夠用一把鎖來管理全部資源,就像下面這樣:
class Account { // 餘額 private Integer balance; // 密碼 private String password; // 取款 synchronized void withdraw(Integer amt) { if (this.balance > amt) { this.balance -= amt; } } // 更改密碼 synchronized void updatePassword(String pw) { this.password = pw; } }
在這裏,我對性能沒有要求,因此只要用 this
這一把鎖,直接管理帳戶的全部資源。
第二,如何保護有關聯的多個資源,這個問題就有點複雜了。
好比,銀行的轉帳操做,不一樣的帳戶是有關聯的,帳戶 A 若是減小 100 元,帳戶 B 就得增長 100 元。這時候,該怎麼避免轉帳的併發問題呢?
你可能想到加鎖,用 synchronized
關鍵詞修飾一下,就像下面這樣:
class Account { // 餘額 private Integer balance; // 轉帳 synchronized void transfer(Account target, Integer amt) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
一把鎖能夠保護多個資源,這看上去沒問題。然而,倒是錯誤的作法。
你想象一下這樣的場景,A、B、C 三個帳戶的餘額都是 100 元。這時候,有兩筆轉帳操做:帳戶 A 轉 100 元到帳戶 B,帳戶 B 轉 100 元到帳戶 C。照理說,結果應該是:帳戶A-0元,帳戶B-100元,帳戶C-200元。
然而,若是是併發轉帳,最終的結果還有兩種可能。一種是:帳戶A-0元,帳戶B-200元,帳戶C-200元;另外一種是:帳戶A-0元,帳戶B-0元,帳戶C-200元。
簡單來講,帳戶 B 的餘額極可能是錯的,問題就出在this
這把鎖上。
還記得嗎?鎖要覆蓋全部受保護的資源。
在這個例子中,卻沒能作到這一點。臨界區內有兩個資源:this.balance
、target.balance
。但this
這把鎖只能保護this.balance
,不能保護target.balance
。
打個比方,你家的鎖就算再厲害,也無法保護鄰居家的東西吧?
所以,咱們要找到一把鎖,同時覆蓋全部帳號。
方案仍是挺多的,最簡單的一個就是:Account.class
。Account.class
共享給全部 Account 對象。並且,Account.class
由 Java 虛擬機建立,具備惟一性。
這樣一來,轉帳的併發問題就解決了,代碼也特別簡單。
class Account { // 餘額 private Integer balance; // 轉帳 void transfer(Account target, Integer amt) { synchronized (Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
在 Java 中,鎖是解決併發的萬能藥,但咱們卻每每用很差,這是由於咱們忽略了鎖與資源的關係。
通常狀況下,這不會有什麼大問題。
然而,一旦出現多個資源,這些資源還相互關聯,就極可能出現公司破產之類的大事。所以,你要時刻記住 3 點:
你只要檢查好這三點,壞事就輪不到你頭上。