国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

[Java并發-3]Java互斥鎖,解決原子性問題

makeFoxPlay / 1587人閱讀

摘要:同一時刻只有一個線程執行這個條件非常重要,我們稱之為互斥。那對于像轉賬這種有關聯關系的操作,我們應該怎么去解決呢先把這個問題代碼化。

在前面的分享中我們提到。

一個或者多個操作在 CPU 執行的過程中不被中斷的特性,稱為“原子性”

思考:在32位的機器上對long型變量進行加減操作存在并發問題,什么原因!?

原子性問題如何解決

我們已經知道原子性問題是線程切換,而操作系統做線程切換是依賴 CPU 中斷的,所以禁止 CPU 發生中斷就能夠禁止線程切換。

在單核 CPU 時代,這個方案的確是可行的。這里我們以 32 位 CPU 上執行 long 型變量的寫操作為例來說明這個問題,long 型變量是 64 位,在 32 位 CPU 上執行寫操作會被拆分成兩次寫操作(寫高 32 位和寫低 32 位,如下圖所示)。

在單核 CPU 場景下,同一時刻只有一個線程執行,禁止 CPU 中斷,獲得 CPU 使用權的線程就可以不間斷地執行,所以兩次寫操作一定是:要么都被執行,要么都沒有被執行,具有原子性。

但是在多核場景下,同一時刻,有可能有兩個線程同時在執行,一個線程執行在 CPU-1 上,一個線程執行在 CPU-2 上,此時禁止 CPU 中斷,只能保證 CPU 上的線程連續執行,并不能保證同一時刻只有一個線程執行,如果這兩個線程同時寫 long 型變量高 32 位的話,還是會出現問題。

同一時刻只有一個線程執行這個條件非常重要,我們稱之為互斥

如果我們能夠保證對共享變量的修改是互斥的,那么,無論是單核 CPU 還是多核 CPU,就都能保證原子性了。

簡易鎖模型

互斥的解決方案,。大家腦中的模型可能是這樣的。

線程在進入臨界區之前,首先嘗試加鎖 lock(),如果成功,則進入臨界區,此時我們稱這個線程持有鎖;否則就等待,直到持有鎖的線程解鎖;持有鎖的線程執行完臨界區的代碼后,執行解鎖 unlock()。

這樣理解本身沒有問題,但卻很容易讓我們忽視兩個非常非常重要的點:

我們鎖的是什么?

我們保護的又是什么?

改進后的鎖模型

我們知道在現實世界里,鎖和鎖要保護的資源是有對應關系的,比如我用我家的鎖保護我家的東西。在并發編程世界里,鎖和資源也應該有這個關系,但這個關系在我們上面的模型中是沒有體現的,所以我們需要完善一下我們的模型。

首先,我們要把臨界區要保護的資源標注出來,如圖中臨界區里增加了一個元素:受保護的資源 R;其次,我們要保護資源 R 就得為它創建一把鎖 LR;最后,針對這把鎖 LR,我們還需在進出臨界區時添上加鎖操作和解鎖操作。另外,在鎖 LR 和受保護資源之間,增加了一條連線,這個關聯關系非常重要,這里很容易發生BUG,容易出現了類似鎖自家門來保護他家資產的事情。

Java語言提供的鎖

鎖是一種通用的技術方案,Java 語言提供的synchronized 關鍵字,就是鎖的一種實現。synchronized關鍵字可以用來修飾方法,也可以用來修飾代碼塊,基本使用:

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

參考我們上面提到的模型,加鎖 lock() 和解鎖 unlock() 這兩個操作在Java 編譯會自動加上。這樣做的好處就是加鎖 lock() 和解鎖 unlock() 一定是成對出現的。

上面的代碼我們看到只有修飾代碼塊的時候,鎖定了一個 obj 對象,那修飾方法的時候鎖定的是什么呢?這個也是 Java 的一條隱式規則:

當修飾靜態方法的時候,鎖定的是當前類的 Class 對象,在上面的例子中就是 Class X;

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

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

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

鎖解決 count+1 問題

我們來嘗試下用synchronized解決之前遇到的 count+=1 存在的并發問題,代碼如下所示。SafeCalc 這個類有兩個方法:一個是 get() 方法,用來獲得 value 的值;另一個是 addOne() 方法,用來給 value 加 1,并且 addOne() 方法我們用 synchronized 修飾。那么我們使用的這兩個方法有沒有并發問題呢?

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

我們先來看看 addOne() 方法,首先可以肯定,被 synchronized 修飾后,無論是單核 CPU 還是多核 CPU,只有一個線程能夠執行 addOne() 方法,所以一定能保證原子操作,那是否有可見性問題呢?
讓我們回顧下之前講一條 Happens-Before的規則。

管程中鎖的規則:對一個鎖的解鎖 Happens-Before 于后續對這個鎖的加鎖。

管程,就是我們這里的 synchronized.我們知道 synchronized 修飾的臨界區是互斥的,也就是說同一時刻只有一個線程執行臨界區的代碼;而這里指的就是前一個線程的解鎖操作對后一個線程的加鎖操作可見.我們就能得出前一個線程在臨界區修改的共享變量(該操作在解鎖之前),對后續進入臨界區(該操作在加鎖之后)的線程是可見的。

按照這個規則,如果多個線程同時執行 addOne() 方法,可見性是可以保證的,也就說如果有 1000 個線程執行 addOne() 方法,最終結果一定是 value 的值增加了 1000。

我們在來看下,執行 addOne() 方法后,value 的值對 get() 方法是可見的嗎?這個可見性是沒法保證的。管程中鎖的規則,是只保證后續對這個鎖的加鎖的可見性,而 get() 方法并沒有加鎖操作,所以可見性沒法保證。那如何解決呢?很簡單,就是 get() 方法也 synchronized 一下,完整的代碼如下所示。

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

上面的代碼轉換為我們提到的鎖模型,就是下面圖示這個樣子。get() 方法和 addOne() 方法都需要訪問 value 這個受保護的資源,這個資源用 this 這把鎖來保護。線程要進入臨界區 get() 和 addOne(),必須先獲得 this 這把鎖,這樣 get() 和 addOne() 也是互斥的。

鎖和受保護資源的關系

我們前面提到,受保護資源和鎖之間的關聯關系非常重要,他們的關系是怎樣的呢?一個合理的關系是:

受保護資源和鎖之間的關聯關系是 N:1 的關系

上面那個例子我稍作改動,把 value 改成靜態變量,把 addOne() 方法改成靜態方法,此時 get() 方法和 addOne() 方法是否存在并發問題呢?

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

如果你仔細觀察,就會發現改動后的代碼是用兩個鎖保護一個資源。這個受保護的資源就是靜態變量 value,兩個鎖分別是 this 和 SafeCalc.class。我們可以用下面這幅圖來形象描述這個關系。由于臨界區 get() 和 addOne() 是用兩個鎖保護的,因此這兩個臨界區沒有互斥關系,臨界區 addOne() 對 value 的修改對臨界區 get() 也沒有可見性保證,這就導致并發問題了。

鎖小結

互斥鎖,在并發領域的知名度極高,只要有了并發問題,大家首先容易想到的就是加鎖,加鎖能夠保證執行臨界區代碼的互斥性。

synchronized 是 Java 在語言層面提供的互斥原語,其實 Java 里面還有很多其他類型的鎖,但作為互斥鎖,原理都是相通的:鎖,一定有一個要鎖定的對象,至于這個鎖定的對象要保護的資源以及在哪里加鎖 / 解鎖,就屬于設計層面的事情。

如何一把鎖保護多個資源? 保護沒有關聯關系的多個資源

當我們要保護多個資源時,首先要區分這些資源是否存在關聯關系。

同樣這對應到編程領域,也很容易解決。例如,銀行業務中有針對賬戶余額(余額是一種資源)的取款操作,也有針對賬戶密碼(密碼也是一種資源)的更改操作,我們可以為賬戶余額和賬戶密碼分配不同的鎖來解決并發問題,這個還是很簡單的。

相關的示例代碼如下,賬戶類 Account 有兩個成員變量,分別是賬戶余額 balance 和賬戶密碼 password。取款 withdraw() 和查看余額 getBalance() 操作會訪問賬戶余額 balance,我們創建一個 final 對象 balLock 作為鎖(類比球賽門票);而更改密碼 updatePassword() 和查看密碼 getPassword() 操作會修改賬戶密碼 password,我們創建一個 final 對象 pwLock 作為鎖(類比電影票)。不同的資源用不同的鎖保護,各自管各自的,很簡單。

class Account {
  // 鎖:保護賬戶余額
  private final Object balLock
    = new Object();
  // 賬戶余額  
  private Integer balance;
  // 鎖:保護賬戶密碼
  private final Object pwLock
    = new Object();
  // 賬戶密碼
  private String password;

  // 取款
  void withdraw(Integer amt) {
    synchronized(balLock) {
      if (this.balance > amt){
        this.balance -= amt;
      }
    }
  } 
  // 查看余額
  Integer getBalance() {
    synchronized(balLock) {
      return balance;
    }
  }

  // 更改密碼
  void updatePassword(String pw){
    synchronized(pwLock) {
      this.password = pw;
    }
  } 
  // 查看密碼
  String getPassword() {
    synchronized(pwLock) {
      return password;
    }
  }
}

當然,我們也可以用一把互斥鎖來保護多個資源,例如我們可以用 this 這一把鎖來管理賬戶類里所有的資源:但是用一把鎖就是性能太差,會導致取款、查看余額、修改密碼、查看密碼這四個操作都是串行的。而我們用兩把鎖,取款和修改密碼是可以并行的。

用不同的鎖對受保護資源進行精細化管理,能夠提升性能 。這種鎖還有個名字,叫 `細粒度鎖`
保護有關聯關系的多個資源

如果多個資源是有關聯關系的,那這個問題就有點復雜了。例如銀行業務里面的轉賬操作,賬戶 A 減少 100 元,賬戶 B 增加 100 元。這兩個賬戶就是有關聯關系的。那對于像轉賬這種有關聯關系的操作,我們應該怎么去解決呢?先把這個問題代碼化。我們聲明了個賬戶類:Account,該類有一個成員變量余額:balance,還有一個用于轉賬的方法:transfer(),然后怎么保證轉賬操作 transfer() 沒有并發問題呢?

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,就像你不能用自家的鎖來保護別人家的資產,也不能用自己的票來保護別人的座位一樣。

下面我們具體分析一下,假設有 A、B、C 三個賬戶,余額都是 200 元,我們用兩個線程分別執行兩個轉賬操作:賬戶 A 轉給賬戶 B 100 元,賬戶 B 轉給賬戶 C 100 元,最后我們期望的結果應該是賬戶 A 的余額是 100 元,賬戶 B 的余額是 200 元, 賬戶 C 的余額是 300 元。

我們假設線程 1 執行賬戶 A 轉賬戶 B 的操作,線程 2 執行賬戶 B 轉賬戶 C 的操作。這兩個線程分別在兩顆 CPU 上同時執行,那它們是互斥的嗎?我們期望是,但實際上并不是。因為線程 1 鎖定的是賬戶 A 的實例(A.this),而線程 2 鎖定的是賬戶 B 的實例(B.this),所以這兩個線程可以同時進入臨界區 transfer()。同時進入臨界區的結果是什么呢?線程 1 和線程 2 都會讀到賬戶 B 的余額為 200,導致最終賬戶 B 的余額可能是 300(線程 1 后于線程 2 寫 B.balance,線程 2 寫的 B.balance 值被線程 1 覆蓋),可能是 100(線程 1 先于線程 2 寫 B.balance,線程 1 寫的 B.balance 值被線程 2 覆蓋),就是不可能是 200。

使用鎖的正確知識

在上一篇文章中,我們提到用同一把鎖來保護多個資源,也就是現實世界的“包場”,那在編程領域應該怎么“包場”呢?很簡單,只要我們的 鎖能覆蓋所有受保護資源 就可以了。

這里我們用 Account.class· 作為共享的鎖。Account.class 是所有 Account 對象共享的,而且這個對象是 Java 虛擬機在加載 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;
      }
    }
  } 
}

下面這幅圖很直觀地展示了我們是如何使用共享的鎖 Account.class 來保護不同對象的臨界區的。

思考下:上面的寫法不是最佳實踐,鎖是可變的。

鎖與資源關系小結

對如何保護多個資源已經很有心得了,關鍵是要分析多個資源之間的關系。如果資源之間沒有關系,很好處理,每個資源一把鎖就可以了。如果資源之間有關聯關系,就要選擇一個粒度更大的鎖,這個鎖應該能夠覆蓋所有相關的資源。除此之外,還要梳理出有哪些訪問路徑,所有的訪問路徑都要設置合適的鎖。

問題:在第一個示例程序里,我們用了兩把不同的鎖來分別保護賬戶余額、賬戶密碼,創建鎖的時候,我們用的是:private final Object xxxLock = new Object();如果賬戶余額用 this.balance 作為互斥鎖,賬戶密碼用 this.password 作為互斥鎖,你覺得是否可以呢?

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77519.html

相關文章

  • [Java并發-12] 原子類:無工具類的典范

    摘要:并發包將這種無鎖方案封裝提煉之后,實現了一系列的原子類。無鎖方案相對互斥鎖方案,最大的好處就是性能。作為一條指令,指令本身是能夠保證原子性的。 前面我們多次提到一個累加器的例子,示例代碼如下。在這個例子中,add10K() 這個方法不是線程安全的,問題就出在變量 count 的可見性和 count+=1 的原子性上。可見性問題可以用 volatile 來解決,而原子性問題我們前面一直都...

    h9911 評論0 收藏0
  • 淺談java中的并發控制

    摘要:并發需要解決的問題功能性問題線程同步面臨兩個問題,想象下有兩個線程在協作工作完成某項任務。鎖可用于規定一個臨界區,同一時間臨界區內僅能由一個線程訪問。并發的數據結構線程安全的容器,如等。 并發指在宏觀上的同一時間內同時執行多個任務。為了滿足這一需求,現代的操作系統都抽象出 線程 的概念,供上層應用使用。 這篇博文不打算詳細展開分析,而是對java并發中的概念和工具做一個梳理。沿著并發模...

    Gilbertat 評論0 收藏0
  • 并發編程導論

    摘要:并發編程導論是對于分布式計算并發編程系列的總結與歸納。并發編程導論隨著硬件性能的迅猛發展與大數據時代的來臨,并發編程日益成為編程中不可忽略的重要組成部分。并發編程復興的主要驅動力來自于所謂的多核危機。 并發編程導論是對于分布式計算-并發編程 https://url.wx-coder.cn/Yagu8 系列的總結與歸納。歡迎關注公眾號:某熊的技術之路。 showImg(https://...

    GeekQiaQia 評論0 收藏0
  • Java 多線程編程基礎——Thread 類

    摘要:程序執行時,至少會有一個線程在運行,這個運行的線程被稱為主線程。程序的終止是指除守護線程以外的線程全部終止。多線程程序由多個線程組成的程序稱為多線程程序。線程休眠期間可以被中斷,中斷將會拋出異常。 線程 我們在閱讀程序時,表面看來是在跟蹤程序的處理流程,實際上跟蹤的是線程的執行。 單線程程序 在單線程程序中,在某個時間點執行的處理只有一個。 Java 程序執行時,至少會有一個線程在運行...

    zhoutk 評論0 收藏0

發表評論

0條評論

makeFoxPlay

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<