成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

makeFoxPlay / 1591人閱讀

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

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

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

思考:在32位的機(jī)器上對long型變量進(jìn)行加減操作存在并發(fā)問題,什么原因?。?/p> 原子性問題如何解決

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

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

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

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

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

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

簡易鎖模型

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

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

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

我們鎖的是什么?

我們保護(hù)的又是什么?

改進(jìn)后的鎖模型

我們知道在現(xiàn)實(shí)世界里,鎖和鎖要保護(hù)的資源是有對應(yīng)關(guān)系的,比如我用我家的鎖保護(hù)我家的東西。在并發(fā)編程世界里,鎖和資源也應(yīng)該有這個關(guān)系,但這個關(guān)系在我們上面的模型中是沒有體現(xiàn)的,所以我們需要完善一下我們的模型。

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

Java語言提供的鎖

鎖是一種通用的技術(shù)方案,Java 語言提供的synchronized 關(guān)鍵字,就是鎖的一種實(shí)現(xiàn)。synchronized關(guān)鍵字可以用來修飾方法,也可以用來修飾代碼塊,基本使用:

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

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

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

當(dāng)修飾靜態(tài)方法的時候,鎖定的是當(dāng)前類的 Class 對象,在上面的例子中就是 Class X;

當(dāng)修飾非靜態(tài)方法的時候,鎖定的是當(dāng)前實(shí)例對象 this。

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

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

鎖解決 count+1 問題

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

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

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

管程中鎖的規(guī)則:對一個鎖的解鎖 Happens-Before 于后續(xù)對這個鎖的加鎖。

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

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

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

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

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

鎖和受保護(hù)資源的關(guān)系

我們前面提到,受保護(hù)資源和鎖之間的關(guān)聯(lián)關(guān)系非常重要,他們的關(guān)系是怎樣的呢?一個合理的關(guān)系是:

受保護(hù)資源和鎖之間的關(guān)聯(lián)關(guān)系是 N:1 的關(guān)系

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

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

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

鎖小結(jié)

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

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

如何一把鎖保護(hù)多個資源? 保護(hù)沒有關(guān)聯(lián)關(guān)系的多個資源

當(dāng)我們要保護(hù)多個資源時,首先要區(qū)分這些資源是否存在關(guān)聯(lián)關(guān)系。

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

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

class Account {
  // 鎖:保護(hù)賬戶余額
  private final Object balLock
    = new Object();
  // 賬戶余額  
  private Integer balance;
  // 鎖:保護(hù)賬戶密碼
  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;
    }
  }
}

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

用不同的鎖對受保護(hù)資源進(jìn)行精細(xì)化管理,能夠提升性能 。這種鎖還有個名字,叫 `細(xì)粒度鎖`
保護(hù)有關(guān)聯(lián)關(guān)系的多個資源

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

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

相信你的直覺會告訴你這樣的解決方案:用戶 synchronized 關(guān)鍵字修飾一下 transfer() 方法就可以了,于是你很快就完成了相關(guān)的代碼,如下所示。

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

在這段代碼中,臨界區(qū)內(nèi)有兩個資源,分別是轉(zhuǎn)出賬戶的余額 this.balance 和轉(zhuǎn)入賬戶的余額 target.balance,并且用的是一把鎖this,符合我們前面提到的,多個資源可以用一把鎖來保護(hù),這看上去完全正確呀。真的是這樣嗎?可惜,這個方案僅僅是看似正確,為什么呢?

問題就出在 this 這把鎖上,this 這把鎖可以保護(hù)自己的余額 this.balance,卻保護(hù)不了別人的余額 target.balance,就像你不能用自家的鎖來保護(hù)別人家的資產(chǎn),也不能用自己的票來保護(hù)別人的座位一樣。

下面我們具體分析一下,假設(shè)有 A、B、C 三個賬戶,余額都是 200 元,我們用兩個線程分別執(zhí)行兩個轉(zhuǎn)賬操作:賬戶 A 轉(zhuǎn)給賬戶 B 100 元,賬戶 B 轉(zhuǎn)給賬戶 C 100 元,最后我們期望的結(jié)果應(yīng)該是賬戶 A 的余額是 100 元,賬戶 B 的余額是 200 元, 賬戶 C 的余額是 300 元。

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

使用鎖的正確知識

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

這里我們用 Account.class· 作為共享的鎖。Account.class 是所有 Account 對象共享的,而且這個對象是 Java 虛擬機(jī)在加載 Account 類的時候創(chuàng)建的,所以我們不用擔(dān)心它的唯一性。

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

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

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

鎖與資源關(guān)系小結(jié)

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

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

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/77519.html

相關(guān)文章

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

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

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

    摘要:并發(fā)需要解決的問題功能性問題線程同步面臨兩個問題,想象下有兩個線程在協(xié)作工作完成某項任務(wù)。鎖可用于規(guī)定一個臨界區(qū),同一時間臨界區(qū)內(nèi)僅能由一個線程訪問。并發(fā)的數(shù)據(jù)結(jié)構(gòu)線程安全的容器,如等。 并發(fā)指在宏觀上的同一時間內(nèi)同時執(zhí)行多個任務(wù)。為了滿足這一需求,現(xiàn)代的操作系統(tǒng)都抽象出 線程 的概念,供上層應(yīng)用使用。 這篇博文不打算詳細(xì)展開分析,而是對java并發(fā)中的概念和工具做一個梳理。沿著并發(fā)模...

    Gilbertat 評論0 收藏0
  • 并發(fā)編程導(dǎo)論

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

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

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

    zhoutk 評論0 收藏0

發(fā)表評論

0條評論

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