寫在前面
上一篇文章原子性問題的巨集觀理解 帶領大家瞭解了鎖和資源的模型,有了這篇文章的鋪墊,相信理解這一篇文章就非常輕鬆了
當我們要保護單個資源並對其進行修改其實很簡單,只需按照下圖分三步走
- 建立受保護資源 R 的鎖
- 加鎖進入臨界區
- 解鎖走出臨界區
上圖的關鍵是「R1 的鎖保護 R1」的指向關係是否正確
如果都是保護單個資源這樣簡單,程式猿的世界該有多美好,可惜並不是,通常我們需要保護多個資源
保護多個資源
保護多個沒有關係的資源
如果多個資源沒有關係,那就是保護一個資源模型的複製,同樣非常簡單,且看下圖:
比如現實中銀行取款和修改密碼操作。
銀行取款操作對應的資源是「餘額」, 修改密碼操作對應的資源是「密碼」,餘額和密碼兩個資源完全沒有關係,所以各自用自家的鎖保護自家的資源就好了
如果多個資源沒有關係,程式猿的世界該有多美好,可惜並不是,我們保護的資源多數情況都有關聯關係
保護多個關係的資源
拿經典的銀行轉賬案例來說明,賬戶 A 給賬戶 B 轉賬,賬戶 A 餘額減少 100 元,賬戶 B 餘額增加 100 元,這個操作要是原子性的,那麼資源「A 餘額」和資源「B 餘額」就這樣"有了關係",先來看程式:
class Account {
private int balance;
// 轉賬
synchronized void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
用 synchronized 直接保護 transfer 方法,然後操作資源「A 餘額」和資源「B 餘額」就可以了
⚠️: 真的是這樣嗎?
先停止向下看,在你的筆記本上按照文章開頭的三步走來畫個圖看一看,是否和下圖一樣呢?
我們通常容易忽略鎖和資源的指向關係,我們想當然的用鎖 this 來保護 target 資源了,也就沒有起到保護作用
假設 A,B,C 賬戶初始餘額都是 200 原,A 向 B 轉賬 100,B 向 C 轉賬 100
我們期盼最終的結果是:
賬戶 A 餘額: 100 元
賬戶 B 餘額: 200 元
賬戶 C 餘額: 300 元
假執行緒 1「A 向 B 轉賬」與執行緒 2「B 向 C 轉賬」兩個操作同時執行,根據 JMM 模型可知,執行緒 1 和執行緒 2 讀取執行緒 B 當前的餘額都是 200 元:
- 執行緒 1 執行 transfer 方法鎖定的是 A 的例項(A.this),並沒有鎖定 B 的例項
- 執行緒 2 執行 transfer 方法鎖定的是 B 的例項(B.this),並沒有鎖定 C 的例項
所以執行緒 1 和執行緒 2 可以同時進入 transfer 臨界區,上面你認為對的模型其實就會變成這個樣子:
還記得 happens-before 規則 這篇文章提到的監視器鎖規則和傳遞性規則嗎?
監視器鎖規則
對一個鎖的解鎖 happens-before 於隨後對這個鎖的加鎖
傳遞性規則
如果 A happens-before B, 且 B happens-before C, 那麼 A happens-before C
資源 B.balance 存在於兩個"臨界區"中,所以這個"臨界區"對 B.balance 來說形同虛設,也就不滿足監視器鎖規則,進而導致傳遞性規則也不生效,說白了,前序執行緒的更改結果對後一個執行緒不可見
這樣最終導致:
- 賬戶 B 的餘額可能是 100: 執行緒 1 寫 B.balance 100(balance = 300) 先於 執行緒 2 寫 B.balance(balance = 100),也就是說執行緒 1 的結果會被執行緒 2 覆蓋,導致最終賬戶 B 的餘額為 100
- 賬戶 B 的餘額可能是 300: 與上述情況相反,執行緒 1 寫 B.balance 100(balance = 300) 後於 執行緒 2 寫 B.balance(balance = 100),也就是說執行緒 2 的結果執行緒 1 覆蓋,導致最終賬戶 B 的餘額為 300
就是不能得到我們理想結果 200,感覺生活無比的艱難,那怎麼辦呢?
正確姿勢
上面的問題就是為資源建立的鎖不能保護所有關聯的資源,那我們就想辦法解決這個問題,來看下面程式碼:
class Account {
private int balance;
// 轉賬
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
我們將 this 鎖變為 Account.class 鎖,Account.class 是虛擬機器載入 Account 類時建立的,肯定是唯一的(雙親委派模型解釋了為何該物件是唯一的), 所有 Account 物件都共享 Account.class, 也就是說,Account.class 鎖能保護所有 Account 物件,我們將上面程式再用模型解釋一下
總結
到這裡關於鎖和資源的關係你應該瞭解的更加透徹了,單個資源和多個無關聯資源的情形都很好處理,為各自資源建立相應的鎖就好,如果多個資源有關聯,為了讓鎖起到保護作用,我們需要將鎖的粒度變大,比如將 this 鎖變成了 Account.class 鎖。
轉賬業務非常常見,併發量非常大,如果我們將鎖的粒度都提升到 Account.class 這個級別(分久必合),假設每次轉賬業務都很耗時,那麼顯然這個鎖的效能是比較低的,所以接下來的文章,我們還會繼續優化這個模型,選擇合適的鎖粒度,同時能保護多個有關聯的資源,
我們的鎖粒度雖然大,但是我們保障了賬戶的安全,所以併發程式設計可以先保證事情做對,遇到瓶頸了,慢慢優化改變相應的模型就好了,當然熟練理解這個模型以後,一步到位的併發程式設計模型當然是極好的......
靈魂追問
- 還記得 happens-before 的幾個原則嗎?
- 偏向鎖,輕量鎖,重量鎖是不是和我們這節內容有異曲同工之處呢?
- 提前想一下,我們如何來優化這個模型呢?
附加說明
如果你對這篇文章理解有些困難,可以按照下面的順序回憶前序文章相關內容
- 這次走進併發的世界,請不要錯過
- 學併發程式設計,透徹理解這三個核心是關鍵
- 併發Bug之源有三,請睜大眼睛看清它們
- 可見性有序性,Happens-before來搞定
- 解決原子性問題?你首先需要的是巨集觀理解
- 面試併發volatile關鍵字時,我們應該具備哪些談資?
推薦閱讀
- 每天用SpringBoot,還不懂RESTful API返回統一資料格式是怎麼實現的?
- 雙親委派模型:大廠高頻面試題,輕鬆搞定
- EasyExcel 讀取 excel 真的很easy
- 紅黑樹,超強動靜圖詳解,簡單易懂
提高效率工具