互斥鎖

???????一個或者多個操作在CPU執行的過程中不被中斷的特性,稱為“原子性”。注意,原子性是面向cpu指令級別操作的,而不是面向高級語言操作。

解決原子性問題

???????帶來原子性問題的是線程切換,如果能夠禁用線程切換,那就能夠解決原子性問題。而操作系統做線程切換是依賴CPU中斷的,所以禁止CPU發生中斷就能夠禁止線程切換。這個方案,在單核CPU時代的確是可行的,而且也有很多應用案例,但是并不適合多核CPU場景。
???????在單核CPU場景下,同一時刻只有一個線程執行,禁止CPU中斷,意味著操作系統不會重新調度線程,也就是禁止了線程切換,獲得CPU使用權的線程就可以不間斷地執行,所以兩次寫操作一定是:要么都被執行,要么都沒有被執行,具有原子性。但是在多核場景下,同一時刻,有可能有兩個線程同時在執行,一個線程執行在CPU-1上,一個線程執行在CPU-2上,此時禁止CPU中斷,只能保證CPU上的線程連續執行,并不能保證同一時刻只有一個線程執行,如果這兩個線程同時寫long型變量高32位的話,那就有可能出現bug了。
???????“同一時刻只有一個線程執行”這個條件非常重要,稱之為互斥。如果能夠保證對共享變量的修改是互斥的,那么無論是單核CPU還是多核CPU,就能保證原子性了。

簡易鎖模型

???????說到互斥,一定會想到“鎖”這個方案,如圖:


簡易鎖模型

??????一段需要互斥執行的代碼稱為臨界區。線程在進入臨界區之前,首先嘗試加鎖lock(),如果成功,則進入臨界區,此時稱這個線程持有鎖;否則,就等待,直到持有鎖的線程解鎖;持有鎖的線程執行完臨界區的代碼后,執行解鎖unlock()。但是,存在兩個極容易忽視的問題:鎖的是什么?保護的又是什么?

改進后的鎖模型

???????鎖和鎖要保護的資源是有對應關系的,上面的模型沒有體現出它們之間的對應關系,故需要完善,如圖:


改進后的鎖模型

???????首先,要把臨界區要保護的資源標注出來,如圖中臨界區里增加了一個元素:受保護的資源R;其次,要保護資源R就得為它創建一把鎖LR;最后,針對這把鎖LR,需要在進出臨界區時添加上加鎖操作和解鎖操作。另外,在鎖LR和受保護資源之間的關聯關系,如圖中的連線,這個關聯關系非常重要。

Java語言提供的鎖技術:synchronized

???????鎖是一種通用的技術方案,Java語言提供的synchronized關鍵字,就是鎖的一種實現。synchronized關鍵字可以用來修飾方法,也可以用來修飾代碼塊,但是他沒有顯示的加鎖lock()和解鎖unlock()操作,這兩個操作是被Java默默加上的,Java編譯器會在synchronized修飾的方法或者代碼塊前后自動加上加鎖lock()和解鎖unlock(),這樣做的好處就是加鎖lock()和解鎖unlock()一定是成對出現的,畢竟忘記解鎖unlock()可是一個致命的bug。
???????Java中有一條隱式規則:當synchronized修飾靜態方法的時候,鎖定的是當前類的Class對象;當synchronized修飾非靜態方式的時候,鎖定的是當前實例對象this;當synchronized修飾代碼塊的時候,鎖定的是其后括號的對象。

鎖和受保護資源的關系

???????一個合理的受保護資源和鎖之間的關聯關系是N:1。例如如下的代碼:

class SafeCalc {
  static long value = 0;
  synchronized long get() {
    return value;
  }
  synchronized static void addOne() {
    value += 1;
  }
}

???????如果仔細觀察,就會發現改動后的代碼是用兩個鎖保護一個資源。這個受保護的資源就是靜態變量value,兩個鎖分別是this和SafeCalc.class。畫一幅圖來描述鎖和受保護資源的關系:


兩把鎖保護一個資源

???????從圖中可知,由于臨界區get()和addOne()是用兩個鎖保護的,因此這兩個臨界區沒有互斥關系,臨界區addOne()對value的修改對臨界區get()也沒有可見性,這就導致并發問題了。所以在并發中,一定要注意受保護資源和鎖之間的關聯關系,是N:1的關系,反之,會導致并發問題。同時,在保護多個資源的時候,首先要區分這些資源是否存在關聯關系。

保護沒有關聯關系的多個資源

???????對于那些沒有關聯關系的多個資源,用不同的鎖來保護,解決并發問題。例如:銀行業務中,有針對賬戶余額的取款操作,也有針對賬戶密碼的更改操作,其中余額(balance)是一種資源,賬戶密碼(password)也是一種資源,為它們分配不同的資源即可解決并發問題。

class Accout {
  //保護余額的鎖
  private final Object balanceLock = new Object;
  //賬戶余額
  private Integer balance;
  //保護賬戶密碼的鎖
  private final Object passwordLock = new Object;
  //賬戶密碼
  private String password;
  //取款
  void withdraw(Integer amt) {
    synchronized(balanceLock) {
      if(this.balance > amt) {
        this.balance -= amt;
      }
    }
  }
  //查看余額
  Integer getBalance() {
    synchronized(balanceLock) {
      return balance;
    }
  }
  //更改密碼
  void updatePassword(String password) {
    synchronized(passwordLock) {
      this.password = password;
    }
  }
  //查看密碼
  String getPassword() {
    synchronized(passwordLock) {
      return password;
    }
  }
}

???????其實,可以用一把鎖來保護多個資源,例如用this這把鎖來管理賬戶類所有的資源,但是用一把鎖,性能會太差,所有的操作都是串行的。故用不同的鎖對沒有關聯關系的資源進行精細化管理,能夠提升性能,稱之為細粒度鎖。

保護有關聯關系的多個資源

???????銀行的轉賬業務中,涉及到兩個存在關聯關系的賬戶:轉出賬戶A和轉入賬戶B。要解決像這樣多個資源是有關聯關系的并發問題,有點復雜。先把問題代碼化,代碼如下:

class Account {
  private int balance;
  //轉賬
  void transfer(Account target, int amt) {
    if(this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  }
}

???????為了解決并發問題,可能會想到用synchronized關鍵字來修飾transfer()方法,代碼如下:

class Account {
  private int balance;
  //轉賬
  synchronized void transfer(Account target, int amt) {
    if(this.balance > amt) {
      this,balance -= amt;
      target.balance += amt;
    }
  }
}

???????從上述代碼中,在臨界區內有兩個資源,分別是轉出賬戶的余額this.balance和轉入賬戶的余額target.balance,用的是this這把鎖,雖然滿足了多個資源可以用一把鎖來保護,但是還是有問題,問題就出在this這把鎖上,this這把鎖可以保護自己的余額this.balance,但是保護不了別人的余額target.balance。看示意圖:


用鎖this保護this.balance和target.balance的示意圖

???????具體分析一下,假設有A、B、C三個賬戶,余額都是200元,用兩個線程分別執行這兩個轉賬操作:賬戶A轉給賬戶B100元,賬戶B轉給賬戶C100元,期望的結果是A的余額是100元,B的余額是200元,C的余額是300元。假設線程1執行賬戶A轉賬給賬戶B的操作,線程2執行賬戶B轉賬給賬戶C的操作。這個線程分別在兩顆CPU上同時執行,但是它們不互斥,因為線程1鎖定的是賬戶A的實例(A.this),而線程2鎖定的是賬戶B的實例(B.this),所以這兩個線程可以同時進入臨界區transfet(),所以有可能線程1和線程2都會讀到賬戶B的余額是為200,導致最終賬戶B的余額可能是300(線程1后于線程2寫B.balance,線程2寫的B.balance值被線程1覆蓋),也可能是100(線程1先于線程2寫B.balance,線程1寫的B.balance值被線程2覆蓋),就是不可能是200。

使用鎖的正確姿勢

???????鎖和受保護的資源的關聯關系是N:1,即可以用同一把鎖來保護多個資源,代碼上,只要使用的鎖能夠覆蓋所有受保護資源就可以實現。實現的方案還是挺多的:

  • 讓所有的對象都持有一個唯一性的對象,但是這個方案缺乏實踐的可行性,因為在創建對象的時候,必須要傳入同一個鎖對象,否則就會出現并發的問題。
  • 使用類的class對象,類的class對象是所有類的實例共享的,而且class對象是Java虛擬機在加載類的時候創建的,不要擔心其唯一性。

總結

???????對于如何保護多個資源,關鍵是要分析多個資源之間的關系。如果資源之間沒有關系,每個資源對應一把鎖即可。如果資源之間有關聯關系,就要選擇一個粒度更大的鎖,這個鎖應該能夠覆蓋所有相關的資源。
???????原子性,其實不是不可分割,不可分割只是外在表現,其本質就是多個資源間有一致性的要求,操作的中間狀態對外不可見。所以要解決原子性問題,是要保證中間狀態對外不可見。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容