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

資訊專欄INFORMATION COLUMN

[Java并發-4]解決Java死鎖的問題

stonezhu / 2976人閱讀

摘要:例如,張三同時申請賬本和,賬本管理員如果發現文件架上只有賬本,這個時候賬本管理員是不會把賬本拿下來給張三的,只有賬本和都在的時候才會給張三。但仍需注意的是,有時候預防死鎖成本也是很高的。

在上一篇中,我們嘗試使用了 Account.class作為互斥鎖,來解決轉賬問題。但是很容易發現這樣,所有的轉賬操作都是串行的,性能太差了。

讓我們嘗試提升下性能。

向現實世界要答案

現實世界中,轉賬操作是支持并發的。

我設想下,在古代沒有信息化的時候。賬戶的存在就是一個個賬本,而且每個用戶都有一個賬本。這些賬本都放在架子上。銀行柜員在轉賬時候,是去架子上同時拿到轉入賬本和轉出賬本,然后做轉賬都。這時候這個柜員會遇到3種情況
1,架子上剛好有 轉入和轉出賬本,同時拿走即可。
2,如果架子上只有轉入和轉出賬本之一,柜員先拿走一本,在等著另一本被送回來。
3,轉入和轉出賬本都沒有,柜員只好等著2個賬本被送回來。

上面的步驟轉換成編碼,其實就是2把鎖實現。轉入賬本一把鎖,轉出賬本一把鎖。在 transfer() 方法內部,我們首先嘗試鎖定轉出賬戶 this(先把轉出賬本拿到手),然后嘗試鎖定轉入賬戶 target(再把轉入賬本拿到手),只有當兩者都成功時,才執行轉賬操作。這個邏輯可以圖形化為下圖這個樣子。

如下所示。經過這樣的優化后,賬戶 A 轉賬戶 B 和賬戶 C 轉賬戶 D 這兩個轉賬操作就可以并行了。

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

相對于用 Account.class 作為互斥鎖,鎖定的范圍太大,而我們鎖定兩個賬戶范圍就小多了,這樣的鎖,上一章我們介紹過,叫細粒度鎖使用細粒度鎖可以提高并行度,是性能優化的一個重要手段。

使用細粒度鎖這么簡單嘛?編寫并發程序就需要這樣時時刻刻保持謹慎。

使用細粒度鎖是有代價的,這個代價就是可能會導致死鎖。

我們還是通過現實世界看一下死鎖產生的原因。如果有客戶找柜員張三做個轉賬業務:賬戶 A 轉賬戶 B 100 元,此時另一個客戶找柜員李四也做個轉賬業務:賬戶 B 轉賬戶 A 100 元,于是張三和李四同時都去文件架上拿賬本,這時候有可能湊巧張三拿到了賬本 A,李四拿到了賬本 B。張三拿到賬本 A 后就等著賬本 B(賬本 B 已經被李四拿走),而李四拿到賬本 B 后就等著賬本 A(賬本 A 已經被張三拿走),他們要等多久呢?他們會永遠等待下去…因為張三不會把賬本 A 送回去,李四也不會把賬本 B 送回去。我們姑且稱為死等吧。

現實世界里的死等,就是編程領域的死鎖了。

死鎖 一組互相競爭資源的線程因互相等待,導致“永久”阻塞的現象
class Account {
  private int balance;
  // 轉賬
  void transfer(Account target, int amt){
    // 鎖定轉出賬戶
    synchronized(this){     ①
      // 鎖定轉入賬戶
      synchronized(target){ ②
        if (this.balance > amt) {
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

關于這種現象,我們還可以借助資源分配圖來可視化鎖的占用情況(資源分配圖是個有向圖,它可以描述資源和線程的狀態)。其中,資源用方形節點表示,線程用圓形節點表示;資源中的點指向線程的邊表示線程已經獲得該資源,線程指向資源的邊則表示線程請求資源,但尚未得到。轉賬發生死鎖時的資源分配圖就如下圖所示。


轉賬發生死鎖時的資源分配圖

如何預防死鎖

并發程序一旦死鎖,一般沒有特別好的方法,很多時候我們只能重啟應用。因此,解決死鎖問題最好的辦法還是規避死鎖。

那如何避免死鎖呢?要避免死鎖就需要分析死鎖發生的條件,有個叫 Coffman 的牛人早就總結過了,只有以下這四個條件都發生時才會出現死鎖:

1,互斥,共享資源 X 和 Y 只能被一個線程占用;
2,占有且等待,線程 T1 已經取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
3,不可搶占,其他線程不能強行搶占線程 T1 占有的資源;
4,循環等待,線程 T1 等待線程 T2 占有的資源,線程 T2 等待線程 T1 占有的資源,就是循環等待。

反過來分析,也就是說只要我們破壞其中一個,就可以成功避免死鎖的發生

其中,互斥這個條件我們沒有辦法破壞,因為我們用鎖為的就是互斥。不過其他三個條件都是有辦法破壞掉的,到底如何做呢?

1,對于“占用且等待”這個條件,我們可以一次性申請所有的資源,這樣就不存在等待了。
2,對于“不可搶占”這個條件,占用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源,這樣不可搶占這個條件就破壞掉了。
3,對于“循環等待”這個條件,可以靠按序申請資源來預防。所謂按序申請,是指資源是有線性順序的,申請的時候可以先申請資源序號小的,再申請資源序號大的,這樣線性化后自然就不存在循環了。

我們已經從理論上解決了如何預防死鎖,下面我們就來嘗試用代碼實踐一下這些理論。

1. 破壞占用且等待條件

從理論上講,要破壞這個條件,可以一次性申請所有資源。在現實世界里,就拿前面我們提到的轉賬操作來講。可以增加一個賬本管理員,然后只允許賬本管理員從文件架上拿賬本,也就是說柜員不能直接在文件架上拿賬本,必須通過賬本管理員才能拿到想要的賬本。例如,張三同時申請賬本 A 和 B,賬本管理員如果發現文件架上只有賬本 A,這個時候賬本管理員是不會把賬本 A 拿下來給張三的,只有賬本 A 和 B 都在的時候才會給張三。這樣就保證了“一次性申請所有資源”。


通過賬本管理員拿賬本圖

對應到編程領域,“同時申請”這個操作是一個臨界區,我們也需要一個角色(Java 里面的類)來管理這個臨界區,我們就把這個角色定為 Allocator。它有兩個重要功能,分別是:同時申請資源 apply() 和同時釋放資源 free()。賬戶 Account 類里面持有一個 Allocator 的單例(必須是單例,只能由一個人來分配資源)。當賬戶 Account 在執行轉賬操作的時候,首先向 Allocator 同時申請轉出賬戶和轉入賬戶這兩個資源,成功后再鎖定這兩個資源;當轉賬操作執行完,釋放鎖之后,我們需通知 Allocator 同時釋放轉出賬戶和轉入賬戶這兩個資源。具體的代碼實現如下。

class Allocator {
  private List als =
    new ArrayList<>();
  // 一次性申請所有資源
  synchronized boolean apply(
    Object from, Object to){
    if(als.contains(from) ||
         als.contains(to)){
      return false;  
    } else {
      als.add(from);
      als.add(to);  
    }
    return true;
  }
  // 歸還資源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
  }
}

class Account {
  // actr 應該為單例
  private Allocator actr;
  private int balance;
  // 轉賬
  void transfer(Account target, int amt){
    // 一次性申請轉出賬戶和轉入賬戶,直到成功
    while(!actr.apply(this, target))
      ;
    try{
      // 鎖定轉出賬戶
      synchronized(this){              
        // 鎖定轉入賬戶
        synchronized(target){           
          if (this.balance > amt){
            this.balance -= amt;
            target.balance += amt;
          }
        }
      }
    } finally {
      actr.free(this, target)
    }
  } 
}

2. 破壞不可搶占條件

破壞不可搶占條件看上去很簡單,核心是要能夠主動釋放它占有的資源,這一點 synchronized 是做不到的。原因是 synchronized 申請資源的時候,如果申請不到,線程直接進入阻塞狀態了,而線程進入阻塞狀態,也釋放不了線程已經占有的資源。java.util.concurrent 這個包下面提供的 Lock 是可以輕松解決這個問題的。關于這個話題,咱們后面會詳細講。

3. 破壞循環等待條件

破壞這個條件,需要對資源進行排序,然后按序申請資源。這個實現非常簡單,我們假設每個賬戶都有不同的屬性 id,這個 id 可以作為排序字段,申請的時候,我們可以按照從小到大的順序來申請。比如下面代碼中,①~⑥處的代碼對轉出賬戶(this)和轉入賬戶(target)排序,然后按照序號從小到大的順序鎖定賬戶。這樣就不存在“循環”等待了。

class Account {
  private int id;
  private int balance;
  // 轉賬
  void transfer(Account target, int amt){
    Account left = this        ①
    Account right = target;    ②
    if (this.id > target.id) { ③
      left = target;           ④
      right = this;            ⑤
    }                          ⑥
    // 鎖定序號小的賬戶
    synchronized(left){
      // 鎖定序號大的賬戶
      synchronized(right){ 
        if (this.balance > amt){
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}
總結

當我們在編程世界里遇到問題時,應不局限于當下,可以換個思路,向現實世界要答案,利用現實世界的模型來構思解決方案,這樣往往能夠讓我們的方案更容易理解,也更能夠看清楚問題的本質。

用細粒度鎖來鎖定多個資源時,要注意死鎖的問題.

預防死鎖主要是破壞三個條件中的一個,有了這個思路后,實現就簡單了。但仍需注意的是,有時候預防死鎖成本也是很高的。例如上面轉賬那個例子,我們破壞占用且等待條件上我們也是鎖了所有的賬戶,而且還是用了死循環 while(!actr.apply(this, target));方法,不過好在 apply() 這個方法基本不耗時。 在轉賬這個例子中,破壞循環等待條件就是成本最低的一個方案。

所以我們在選擇具體方案的時候,還需要評估一下操作成本,從中選擇一個成本最低的方案

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

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

相關文章

  • 并發編程之死鎖解析

    摘要:如何檢測死鎖由于死鎖極難通過人工的方式查出來,因此提供了命令來檢測某個進程中心線程的情況,并排查有沒有死鎖。線程持有的鎖,等待的鎖。避免出現死鎖,如果出現了死鎖,則可以使用命令查看線程是否有死鎖。 showImg(https://segmentfault.com/img/remote/1460000014936757); 前言 在 Java 的并發編程中,有一個問題需要特別注意,那就是...

    yy736044583 評論0 收藏0
  • Java多線程學習(七)并發編程中一些問題

    摘要:因為多線程競爭鎖時會引起上下文切換。減少線程的使用。舉個例子如果說服務器的帶寬只有,某個資源的下載速度是,系統啟動個線程下載該資源并不會導致下載速度編程,所以在并發編程時,需要考慮這些資源的限制。 最近私下做一項目,一bug幾日未解決,總惶恐。一日頓悟,bug不可怕,怕的是項目不存在bug,與其懼怕,何不與其剛正面。 系列文章傳送門: Java多線程學習(一)Java多線程入門 Jav...

    yimo 評論0 收藏0
  • Java多線程學習(七)并發編程中一些問題

    摘要:相比與其他操作系統包括其他類系統有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。因為多線程競爭鎖時會引起上下文切換。減少線程的使用。很多編程語言中都有協程。所以如何避免死鎖的產生,在我們使用并發編程時至關重要。 系列文章傳送門: Java多線程學習(一)Java多線程入門 Java多線程學習(二)synchronized關鍵字(1) java多線程學習(二)syn...

    dingding199389 評論0 收藏0
  • 多線程編程完全指南

    摘要:在這個范圍廣大的并發技術領域當中多線程編程可以說是基礎和核心,大多數抽象并發問題的構思與解決都是基于多線程模型來進行的。一般來說,多線程程序會面臨三類問題正確性問題效率問題死鎖問題。 多線程編程或者說范圍更大的并發編程是一種非常復雜且容易出錯的編程方式,但是我們為什么還要冒著風險艱辛地學習各種多線程編程技術、解決各種并發問題呢? 因為并發是整個分布式集群的基礎,通過分布式集群不僅可以大...

    mengera88 評論0 收藏0
  • 一起學并發編程 - 死鎖跟蹤分析

    摘要:上一章介紹過關鍵字,使用它可以給程序互斥部分加上一把鎖從而達到同步的效果,但錯誤的用法會導致多個線程同時被阻塞死鎖死鎖多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不可能正常終止。 上一章介紹過synchronized關鍵字,使用它可以給程序互斥部分加上一把鎖從而達到同步的效果,但錯誤的用法會導致多個線程同時被阻塞.... 死鎖 死鎖...

    ACb0y 評論0 收藏0

發表評論

0條評論

stonezhu

|高級講師

TA的文章

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