共享資源那麼多,如何用一把鎖保護多個資源?

寫在前面

上一篇文章原子性問題的宏觀理解 帶領你們瞭解了鎖和資源的模型,有了這篇文章的鋪墊,相信理解這一篇文章就很是輕鬆了html

當咱們要保護單個資源並對其進行修改其實很簡單,只需按照下圖分三步走java

  1. 建立受保護資源 R 的鎖
  2. 加鎖進入臨界區
  3. 解鎖走出臨界區

上圖的關鍵是「R1 的鎖保護 R1」的指向關係是否正確面試

若是都是保護單個資源這樣簡單,程序猿的世界該有多美好,惋惜並非,一般咱們須要保護多個資源編程

保護多個資源

保護多個沒有關係的資源

若是多個資源沒有關係,那就是保護一個資源模型的複製,一樣很是簡單,且看下圖:安全

好比現實中銀行取款和修改密碼操做。 銀行取款操做對應的資源是「餘額」, 修改密碼操做對應的資源是「密碼」,餘額和密碼兩個資源徹底沒有關係,因此各自用自家的鎖保護自家的資源就行了併發

若是多個資源沒有關係,程序猿的世界該有多美好,惋惜並非,咱們保護的資源多數狀況都有關聯關係app

保護多個關係的資源

拿經典的銀行轉帳案例來講明,帳戶 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 這個級別(分久必合),假設每次轉帳業務都很耗時,那麼顯然這個鎖的性能是比較低的,因此接下來的文章,咱們還會繼續優化這個模型,選擇合適的鎖粒度,同時能保護多個有關聯的資源

咱們的鎖粒度雖然大,可是咱們保障了帳戶的安全,因此併發編程能夠先保證事情作對,遇到瓶頸了,慢慢優化改變相應的模型就行了,固然熟練理解這個模型之後,一步到位的併發編程模型固然是極好的......

靈魂追問

  1. 還記得 happens-before 的幾個原則嗎?
  2. 偏向鎖,輕量鎖,重量鎖是否是和咱們這節內容有殊途同歸之處呢?
  3. 提早想一下,咱們如何來優化這個模型呢?

附加說明

若是你對這篇文章理解有些困難,能夠按照下面的順序回憶前序文章相關內容

  1. 此次走進併發的世界,請不要錯過
  2. 學併發編程,透徹理解這三個核心是關鍵
  3. 併發Bug之源有三,請睜大眼睛看清它們
  4. 可見性有序性,Happens-before來搞定
  5. 解決原子性問題?你首先須要的是宏觀理解
  6. 面試併發volatile關鍵字時,咱們應該具有哪些談資?

推薦閱讀


提升效率工具


歡迎持續關注公衆號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......

相關文章
相關標籤/搜索