互斥鎖,解決原子性問題

併發編程有3個源頭性問題:緩存致使的可見性問題,編譯優化致使的有序性問題,以及線程切換致使的原子性問題。解決可見性問題和有序性問題的方法是按需禁用緩存和編譯優化,Java的內存模型就是一種按需禁用緩存和編譯優化的規則,它規定了 JVM 如何提供相關的方法,這些已經在Java內存模型與Hppens-Before規則進行了描述。java

互斥鎖,解決原子性問題

咱們把一個或者多個操做在 CPU 執行過程當中不被中斷的特性稱爲原子性。因爲操做系統的時間片輪起色制,以及高級語言可能包含多個指令,致使一句高級語言在執行過程當中可能出現線程切換。在併發編程中就會由於線程切換致使原子性問題編程

鎖模型是解決原子性問題的通用方案。線程在進入臨界區以前必須持有鎖,退出臨界區時釋放鎖,此時其餘線程就能再次獲取鎖。segmentfault

鎖與資源之間是 1:N 的關係,即一把鎖能夠保護多個資源。同時要注意不能用本身的鎖保護別人的資源;要讓代碼實現互斥,必須使用同一把鎖。緩存

synchronized 關鍵字是 Java 語言對鎖模型的實現,它能夠修飾方法或者代碼塊,被修飾的方法和代碼塊會隱式地添加lock()unlock()方法。併發

什麼是原子性問題

現代操做系統都是基於線程的分時調度系統,CPU會爲線程分配時間片,線程分配都時間片就獲取到CPU的使用權。好比說線程 A 讀取文件,它能夠將本身標記爲「休眠狀態」,讓出 CPU 的使用權。文件讀取完成以後,操做系統再將其喚醒,線程 A 就有機會從新得到 CPU 的使用權。app

線程切換爲何致使併發問題呢?Java 是一門高級語言,高級語言的一條語句每每包含多個 CPU 指定,好比說 count += 1 這條語句,至少包含 3 條 CPU 指令:優化

  1. 把變量 count 從內存加載到 CPU 的寄存器;
  2. 在寄存器中執行 +1 操做;
  3. 將結果寫入內存(緩存機制致使可能寫入的是 CPU 緩存而不是內存)。

操做系統以指令爲單位執行,期間伴隨着線程切換。這就致使 count += 1 執行到一半,就有可能碰到線程切換,致使併發問題的產生,以下圖所示:this

線程切換致使的原子性問題

咱們把一個或者多個操做在 CPU 執行過程當中不被中斷的特性稱爲原子性,即咱們指望 count += 1 在執行過程當中是原子同樣的,不可分割的總體,線程切換不會在執行這條語句相關的CPU指令時發生,但容許線程切換在count += 1執行以前或者以後發生。spa

鎖模型

鎖模型是一種解決原子性問題的通用技術方案。在鎖模型中,臨界區是一段要互斥執行的代碼,在進入臨界區以前咱們要執行 lock() 操做持有鎖,只有獲取到鎖的線程才能執行臨界區的代碼;執行完臨界區代碼執行 unlock() 操做釋放鎖,此時其餘線程就能夠嘗試獲取鎖。操作系統

鎖模型

在現實生活中,咱們用鎖來保護咱們的東西,但不能用本身的鎖來鎖別人的東西。在鎖模型中,鎖與臨界區中被保護的資源也有着關聯關係,圖中用箭頭來表示它們之間的關聯。

咱們不能用一把鎖來保護範圍以外的資源,代碼要實現互斥則要使用同一把鎖。

Java 語言提供的鎖技術:synchronized

鎖是一種通用的技術方案,Java 語言提供的 synchronized 關鍵字,就是鎖的一種實現。synchronized 關鍵字能夠用來修飾方法,也能夠用來修飾代碼塊,它的使用示例基本上都是下面這個樣子:

class X {
  // 修飾非靜態方法
  synchronized void foo() {
    // 臨界區
  }
  // 修飾靜態方法
  synchronized static void bar() {
    // 臨界區
  }
  // 修飾代碼塊
  Object obj = new Object();
  void baz() {
    synchronized(obj) {
      // 臨界區
    }
  }
}

前面說過,鎖模型中有鎖以及它保護的資源,synchronized 修飾代碼塊的時候鎖顯然是 obj 對象,那麼 synchronized 修飾非靜態方法和靜態方法的時候,它建立的鎖是什麼呢?

Java 中有一條隱式規則:

當修飾靜態方法的時候,鎖定的是當前類的 Class 對象;
當修飾非靜態方法的時候,鎖定的是當前實例對象 this。

至關於

class X {
  // 修飾靜態方法
  synchronized(X.class) static void bar() {
    // 臨界區
  }
}

class X {
  // 修飾非靜態方法
  synchronized(this) void foo() {
    // 臨界區
  }
}

鎖和受保護資源的關係

鎖能夠保護一個或者多個資源。咱們能夠用一個範圍較大的鎖,好比說 X.class 保護多個相關的資源;也能夠用不一樣的鎖對被保護資源進行精細化管理,這就叫細粒度鎖

一把鎖保護一個資源

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

這是一段想解決 count += 1 問題的代碼,咱們對 addOne() 使用 synchronized 加上互斥鎖,能夠保證其原子性。根據 Happens-before 管程中鎖的規則:對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖,也能夠保證其可見性。即便是 1000 個線程同時執行 addOne() 也能夠保證 value 增長 1000。

但咱們沒法保證 get() 的可見性,管程中鎖的規則,是隻保證後續對這個鎖的加鎖的可見性,而 get() 方法並無加鎖操做,因此可見性無法保證。因此咱們給 get() 也加上鎖:

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

此時 get()addOne() 都持有 this 這把鎖,此時 get()addOne() 是互斥的,而且保證了可見性,縮模型以下圖所示:

一把鎖保護一個資源

兩把鎖保護不一樣資源的問題

若是將 value 改成 static 的,addOne() 變爲靜態方法:

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

代碼不互斥

此時 get()addOne() 分別持有不一樣的鎖,get()addOne() 不互斥,也就不能保證可見性,就會致使併發問題。

一把鎖保護多個資源

如今要寫一個銀行轉帳的方法,用戶 A 給用戶 B 轉帳,將其轉換成代碼:

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

用戶 A 給用戶 B 轉帳 100,要保證 A 的餘額減小 100,B 的餘額增長 100。因爲轉帳操做能夠是併發的,因此要保證轉帳操做沒有併發問題。好比說 A 的餘額只有 100,兩個線程分別執行 A 給 B 轉帳 100,A 給 C 轉帳 100,這兩個線程有可能同時從內存中讀取到 A 的餘額是 100,這就產生了併發問題。

解決這個問題的第一反應,就是給 transfer(Account target, int amt) 加上 synchronized。這樣作真的對麼?transfer() 此時有兩個須要被保護的資源 target.balancethis.balance 即別人錢和本身的錢,但咱們使用的鎖是 this 鎖,以下圖所示:

鎖沒法鎖住別人的資源

本身的鎖 this 能保護本身的 this.balance 可是沒法保護別人的 target.balance,就像個人鎖不能即保護我家的東西,又保護你家的東西同樣。

因此咱們須要一把鎖的範圍更大一點,讓它可以覆蓋到全部的被保護資源,好比說傳入同一個對象做爲鎖:

class Account {
  private Object lock;
  private int balance;
  private Account();
  // 建立Account時傳入同一個lock對象
  public Account(Object lock) {
    this.lock = lock;
  } 
  // 轉帳
  void transfer(Account target, int amt){
    // 此處檢查全部對象共享的鎖
    synchronized(lock) {
      if (this.balance > amt) {
        this.balance -= amt;
        target.balance += amt;
      }
    }
  }
}

或者使用類鎖 Accout.class,因爲 Accoutn.class 是在 Java 虛擬機加載 Account 類時建立的,因此 Account.class 是全部 Account 對象共享且惟一的一把鎖。

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

Accout.class 就能夠同時保護兩個不一樣對象的臨界區資源:

更粗粒度的鎖

相關文章

03 | 互斥鎖(上):解決原子性問題

04 | 互斥鎖(下):如何用一把鎖保護多個資源?

相關文章
相關標籤/搜索