死鎖面試題(史上最強)


系列:若是整個 地表最強 的開發環境?

工欲善其事 必先利其器
地表最強 開發環境: vagrant+java+springcloud+redis+zookeeper鏡像下載(&製做詳解)
地表最強 熱部署:java SpringBoot SpringCloud 熱部署 熱加載 熱調試
地表最強 發請求工具(再見吧, PostMan ):IDEA HTTP Client(史上最全)
地表最強 PPT 小工具: 屌炸天,像寫代碼同樣寫PPT
無編程不創客,瘋狂創客圈,一大波編程高手正在交流、學習中!,GO

瘋狂創客圈 springCloud 高併發系列

推薦閱讀
nacos 實戰(史上最全)
sentinel (史上最全+入門教程)
springcloud + webflux 高併發實戰
Webflux(史上最全)
SpringCloud gateway (史上最全)
無編程不創客,瘋狂創客圈,一大波編程高手正在交流、學習中!,GO

導讀:

首先介紹大廠的死鎖面試題,而後對死鎖作一個全面的解讀。web

大廠的死鎖面試題

什麼是死鎖?

所謂死鎖,是指多個進程在運行過程當中因爭奪資源而形成的一種僵局,當進程處於這種僵持狀態時,若無外力做用,它們都將沒法再向前推動。 所以咱們舉個例子來描述,若是此時有一個線程A,按照先鎖a再得到鎖b的的順序得到鎖,而在此同時又有另一個線程B,按照先鎖b再鎖a的順序得到鎖。以下圖所示:面試

img

產生死鎖的緣由?

可歸結爲以下兩點:redis

a. 競爭資源算法

  • 系統中的資源能夠分爲兩類:
  1. 可剝奪資源,是指某進程在得到這類資源後,該資源能夠再被其餘進程或系統剝奪,CPU和主存均屬於可剝奪性資源;
  2. 另外一類資源是不可剝奪資源,當系統把這類資源分配給某進程後,再不能強行收回,只能在進程用完後自行釋放,如磁帶機、打印機等。
  • 產生死鎖中的競爭資源之一指的是競爭不可剝奪資源(例如:系統中只有一臺打印機,可供進程P1使用,假定P1已佔用了打印機,若P2繼續要求打印機打印將阻塞)
  • 產生死鎖中的競爭資源另一種資源指的是競爭臨時資源(臨時資源包括硬件中斷、信號、消息、緩衝區內的消息等),一般消息通訊順序進行不當,則會產生死鎖

b. 進程間推動順序非法spring

  • 若P1保持了資源R1,P2保持了資源R2,系統處於不安全狀態,由於這兩個進程再向前推動,即可能發生死鎖
  • 例如,當P1運行到P1:Request(R2)時,將因R2已被P2佔用而阻塞;當P2運行到P2:Request(R1)時,也將因R1已被P1佔用而阻塞,因而發生進程死鎖

死鎖產生的4個必要條件?

產生死鎖的必要條件:編程

  1. 互斥條件:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅爲一進程所佔用。
  2. 請求和保持條件:當進程因請求資源而阻塞時,對已得到的資源保持不放。
  3. 不剝奪條件:進程已得到的資源在未使用完以前,不能剝奪,只能在使用完時由本身釋放。
  4. 環路等待條件:在發生死鎖時,必然存在一個進程--資源的環形鏈。

解決死鎖的基本方法

1、預防死鎖:

  • 資源一次性分配:一次性分配全部資源,這樣就不會再有請求了:(破壞請求條件)
  • 只要有一個資源得不到分配,也不給這個進程分配其餘的資源:(破壞請保持條件)
  • 可剝奪資源:即當某進程得到了部分資源,但得不到其它資源,則釋放已佔有的資源(破壞不可剝奪條件)
  • 資源有序分配法:系統給每類資源賦予一個編號,每個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)

1 以肯定的順序得到鎖

若是必須獲取多個鎖,那麼在設計的時候須要充分考慮不一樣線程以前得到鎖的順序。按照上面的例子,兩個線程得到鎖的時序圖以下:安全

img

若是此時把得到鎖的時序改爲:

img

那麼死鎖就永遠不會發生。 針對兩個特定的鎖,開發者能夠嘗試按照鎖對象的hashCode值大小的順序,分別得到兩個鎖,這樣鎖老是會以特定的順序得到鎖,那麼死鎖也不會發生。問題變得更加複雜一些,若是此時有多個線程,都在競爭不一樣的鎖,簡單按照鎖對象的hashCode進行排序(單純按照hashCode順序排序會出現「環路等待」),可能就沒法知足要求了,這個時候開發者可使用銀行家算法,全部的鎖都按照特定的順序獲取,一樣能夠防止死鎖的發生,該算法在這裏就再也不贅述了,有興趣的能夠自行了解一下。

2 超時放棄

當使用synchronized關鍵詞提供的內置鎖時,只要線程沒有得到鎖,那麼就會永遠等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,該方法能夠按照固定時長等待鎖,所以線程能夠在獲取鎖超時之後,主動釋放以前已經得到的全部的鎖。經過這種方式,也能夠頗有效地避免死鎖。 仍是按照以前的例子,時序圖以下:

img

2、避免死鎖:

  • 預防死鎖的幾種策略,會嚴重地損害系統性能。所以在避免死鎖時,要施加較弱的限制,從而得到 較滿意的系統性能。因爲在避免死鎖的策略中,容許進程動態地申請資源。於是,系統在進行資源分配以前預先計算資源分配的安全性。若這次分配不會致使系統進入不安全的狀態,則將資源分配給進程;不然,進程等待。其中最具備表明性的避免死鎖算法是銀行家算法。
  • 銀行家算法:首先須要定義狀態和安全狀態的概念。系統的狀態是當前給進程分配的資源狀況。所以,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給進程的每種資源的總量)及兩個矩陣Claim(表示進程對資源的需求)和Allocation(表示當前分配給進程的資源)。安全狀態是指至少有一個資源分配序列不會致使死鎖。當進程請求一組資源時,假設贊成該請求,從而改變了系統的狀態,而後肯定其結果是否還處於安全狀態。若是是,贊成這個請求;若是不是,阻塞該進程知道贊成該請求後系統狀態仍然是安全的。

3、檢測死鎖

  1. 首先爲每一個進程和每一個資源指定一個惟一的號碼;

  2. 而後創建資源分配表和進程等待表。

    死鎖檢測的工具

    一、Jstack命令

    jstack是java虛擬機自帶的一種堆棧跟蹤工具。jstack用於打印出給定的java進程ID或core file或遠程調試服務的Java堆棧信息。 Jstack工具能夠用於生成java虛擬機當前時刻的線程快照。線程快照是當前java虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的緣由,如線程間死鎖、死循環、請求外部資源致使的長時間等待等。 線程出現停頓的時候經過jstack來查看各個線程的調用堆棧,就能夠知道沒有響應的線程到底在後臺作什麼事情,或者等待什麼資源。

    二、JConsole工具

    Jconsole是JDK自帶的監控工具,在JDK/bin目錄下能夠找到。它用於鏈接正在運行的本地或者遠程的JVM,對運行在Java應用程序的資源消耗和性能進行監控,並畫出大量的圖表,提供強大的可視化界面。並且自己佔用的服務器內存很小,甚至能夠說幾乎不消耗。

4、解除死鎖:

當發現有進程死鎖後,便應當即把它從死鎖狀態中解脫出來,常採用的方法有:

  • 剝奪資源:從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態;
  • 撤消進程:能夠直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除爲止;所謂代價是指優先級、運行代價、進程的重要性和價值等。

ok,介紹大廠的死鎖面試題以後,接下來,對死鎖作一個全面的解讀。

1、什麼是死鎖

多線程以及多進程改善了系統資源的利用率並提升了系統 的處理能力。然而,併發執行也帶來了新的問題——死鎖。
死鎖是指兩個或兩個以上的進程(線程)在運行過程當中因爭奪資源而形成的一種僵局(Deadly-Embrace) ) ,若無外力做用,這些進程(線程)都將沒法向前推動。

下面咱們經過一些實例來講明死鎖現象。

先看生活中的一個實例,2我的一塊兒吃飯可是隻有一雙筷子,2人輪流吃(同時擁有2只筷子才能吃)。某一個時候,一個拿了左筷子,一人拿了右筷子,2我的都同時佔用一個資源,等待另外一個資源,這個時候甲在等待乙吃完並釋放它佔有的筷子,同理,乙也在等待甲吃完並釋放它佔有的筷子,這樣就陷入了一個死循環,誰也沒法繼續吃飯。。。
在計算機系統中也存在相似的狀況。例如,某計算機系統中只有一臺打印機和一臺輸入設備,進程P1正佔用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2 所佔用,而P2在未釋放打印機以前,又提出請求使用正被P1佔用着的輸入設備。這樣兩個進程相互無休止地等待下去,均沒法繼續執行,此時兩個進程陷入死鎖狀態。

關於死鎖的一些結論:

  • 參與死鎖的進程數至少爲兩個
  • 參與死鎖的全部進程均等待資源
  • 參與死鎖的進程至少有兩個已經佔有資源
  • 死鎖進程是系統中當前進程集合的一個子集
  • 死鎖會浪費大量系統資源,甚至致使系統崩潰。

舉一個例子:

如何解決上面的問題呢?正所謂知己知彼方能百戰不殆,咱們要先了解什麼狀況會發生死鎖,才能知道如何避免死鎖,很幸運咱們能夠站在巨人的肩膀上看待問題

一個銀行轉帳經典案例:

帳戶 A 給帳戶 B 轉帳,帳戶 A 餘額減小 100 元,帳戶 B 餘額增長 100 元,這個操做要是原子性的

先來看程序:

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

用 synchronized 直接保護 transfer 方法,而後操做 資源「Account 的A 餘額」和資源「Account的B 餘額」就能夠了

其中有兩個問題:

  1. 單純的用 synchronized 方法起不到保護做用(不能保護 target)
  2. 用 Account.class 鎖方案,鎖的粒度又過大,致使涉及到帳戶的全部操做(取款,轉帳,修改密碼等)都會變成串行操做

如何解決這兩個問題呢?我們先換好衣服穿越回到過去尋找一下錢莊,一塊兒透過現象看本質,dengdeng deng.......

在這裏插入圖片描述

來到錢莊,告訴櫃員你要給鐵蛋兒轉 100 銅錢,這時櫃員轉身在牆上尋找你和鐵蛋兒的帳本,此時櫃員可能面臨三種狀況:

  1. 理想狀態: 你和鐵蛋兒的帳本都是空閒狀態,一塊兒拿回來,在你的帳本上減 100 銅錢,在鐵蛋兒帳本上加 100 銅錢,櫃員轉身將帳本掛回到牆上,完成你的業務
  2. 尷尬狀態: 你的帳本在,鐵蛋兒的帳本被其餘櫃員拿出去給別人轉帳,你要等待其餘櫃員把鐵蛋兒的帳本歸還
  3. 抓狂狀態: 你的帳本不在,鐵蛋兒的帳本也不在,你只能等待兩個帳本都歸還

放慢櫃員的取帳本操做,他必定是先拿到你的帳本,而後再去拿鐵蛋兒的帳本,兩個帳本都拿到(理想狀態)以後才能完成轉帳,用程序模型來描述一下這個拿取帳本的過程:

咱們繼續用程序代碼描述一下上面這個模型:

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;
        }
      }
    }
  } 
}

這個解決方案看起來很完美,解決了文章開頭說的兩個問題,但真是這樣嗎?

咱們剛剛說過的理想狀態是錢莊只有一個櫃員(既單線程)。隨着錢莊規模變大,牆上早已掛了很是多個帳本,錢莊爲了應對繁忙的業務,開通了多個窗口,此時有多個櫃員(多線程)處理錢莊業務。

櫃員 1 正在辦理給鐵蛋兒轉帳的業務,但只拿到了你的帳本;櫃員 2 正在辦理鐵蛋兒給你轉帳的業務,但只拿到了鐵蛋兒的帳本,此時雙方出現了尷尬狀態,兩位櫃員都在等待對方歸還帳本爲當前客戶辦理轉帳業務。

在這裏插入圖片描述

現實中櫃員會溝通,喊出一嗓子 老鐵,鐵蛋兒的帳本先給我用一下,用完還給你,但程序卻沒這麼智能,synchronized 內置鎖很是執着,它會告訴你「死等」的道理,最終出現死鎖

如何解決死鎖呢?

Java 有了 synchronized 內置鎖,還發明瞭顯示鎖 Lock,是否是就爲了治一治 synchronized 「死等」的執着呢??

若是你捉急,能夠直接去閱讀第四節。

2、死鎖與飢餓

飢餓(Starvation)指一個進程一直得不到資源。

死鎖和飢餓都是因爲進程競爭資源而引發的。飢餓通常不佔有資源,死鎖進程必定佔有資源。

3、資源的類型

3.1 可重用資源和消耗性資源

3.1.1 可重用資源(永久性資源)

可被多個進程屢次使用,如全部硬件。

  • 只能分配給一個進程使用,不容許多個進程共享。
  • 進程在對可重用資源的使用時,須按照請求資源、使用資源、釋放資源這樣的順序。
  • 系統中每一類可重用資源中的單元數目是相對固定的,進程在運行期間,既不能建立,也不能刪除。

3.1.2 消耗性資源(臨時性資源)

又稱臨時性資源,是由進程在運行期間動態的建立和消耗的。

  • 消耗性資源在進程運行期間是能夠不斷變化的,有時可能爲0。
  • 進程在運行過程當中,能夠不斷地創造可消耗性資源的單元,將它們放入該資源類的緩衝區中,以增長該資源類的單元數目。
  • 進程在運行過程當中,能夠請求若干個可消耗性資源單元,用於進程本身消耗,再也不將它們返回給該資源類中。

可消耗資源一般是由生產者進程建立,由消費者進程消耗。最典型的可消耗資源是用於進程間通訊的消息。

3.2 可搶佔資源和不可搶佔資源

3.2.1 可搶佔資源

可搶佔資源指某進程在得到這類資源後,該資源能夠再被其餘進程或系統搶佔。對於這類資源是不會引發死鎖的。

CPU 和主存均屬於可搶佔性資源。

3.2.2 不可搶佔資源

一旦系統把某資源分配給該進程後,就不能將它強行收回,只能在進程用完後自行釋放。

磁帶機、打印機等屬於不可搶佔性資源。

4、死鎖產生的緣由

  • 競爭不可搶佔資源引發死鎖
    一般系統中擁有的不可搶佔資源,其數量不足以知足多個進程運行的須要,使得進程在運行過程當中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可搶佔資源的競爭 纔可能產生死鎖,對可搶佔資源的競爭是不會引發死鎖的。

  • 競爭可消耗資源引發死鎖

  • 進程推動順序不當引發死鎖
    進程在運行過程當中,請求和釋放資源的順序不當,也一樣會致使死鎖。例如,併發進程 P一、P2分別保持了資源R一、R2,而進程P1申請資源R2,進程P2申請資源R1時,二者都會由於所需資源被佔用而阻塞。
    信號量使用不當也會形成死鎖。進程間彼此相互等待對方發來的消息,結果也會使得這 些進程間沒法繼續向前推動。例如,進程A等待進程B發的消息,進程B又在等待進程A 發的消息,能夠看出進程A和B不是由於競爭同一資源,而是在等待對方的資源致使死鎖。

4.1 競爭不可搶佔資源引發死鎖

如:共享文件時引發死鎖
系統中擁有兩個進程P1和P2,它們都準備寫兩個文件F1和F2。而這二者都屬於可重用和不可搶佔性資源。若是進程P1在打開F1的同時,P2進程打開F2文件,當P1想打開F2時因爲F2已結被佔用而阻塞,當P2想打開1時因爲F1已結被佔用而阻塞,此時就會無線等待下去,造成死鎖。
這裏寫圖片描述

4.2 競爭可消耗資源引發死鎖

如:進程通訊時引發死鎖
系統中擁有三個進程P一、P2和P3,m一、m二、m3是3可消耗資源。進程P1一方面產生消息m1,將其發送給P2,另外一方面要從P3接收消息m3。而進程P2一方面產生消息m2,將其發送給P3,另外一方面要從P1接收消息m1。相似的,進程P3一方面產生消息m3,將其發送給P1,另外一方面要從P2接收消息m2。
若是三個進程都先發送本身產生的消息後接收別人發來的消息,則能夠順利的運行下去不會產生死鎖,但要是三個進程都先接收別人的消息而不產生消息則會永遠等待下去,產生死鎖。
這裏寫圖片描述

4.3 進程推動順序不當引發死鎖

這裏寫圖片描述
上圖中,若是按曲線1的順序推動,兩個進程可順利完成;若是按曲線2的順序推動,兩個進程可順利完成;若是按曲線3的順序推動,兩個進程可順利完成;若是按曲線4的順序推動,兩個進程將進入不安全區D中,此時P1保持了資源R1,P2保持了資源R2,系統處於不安全狀態,若是繼續向前推動,則可能產生死鎖。

5、產生死鎖的四個必要條件

Coffman 總結出了四個條件說明能夠發生死鎖的情形:

Coffman 條件

互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。若是此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。

不可剝奪條件:指進程已得到的資源,在未使用完以前,不能被剝奪,只能在使用完時由本身釋放。

請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對本身已得到的其它資源保持不放。

環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P1,P2,···,Pn}中的 P1 正在等待一個 P2 佔用的資源;P2 正在等待 P3 佔用的資源,……,Pn 正在等待已被 P0 佔用的資源。

在這裏插入圖片描述

這幾個條件很好理解,其中「互斥條件」是併發編程的根基,這個條件沒辦法改變。但其餘三個條件都有改變的可能,也就是說破壞另外三個條件就不會出現上面說到的死鎖問題

5.1 互斥條件:

進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內某資源僅爲一個進程所佔有。此時如有其餘進程請求該資源,則請求進程只能等待。

5.2 不可剝奪條件:

進程所得到的資源在未使用完畢以前,不能被其餘進程強行奪走,即只能由得到該資源的進程本身來釋放(只能是主動釋放)。

5.3 請求與保持條件:

進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其餘進程佔有,此時請求進程被阻塞,但對本身已得到的資源保持不放。

5.4 循環等待條件:

存在一種進程資源的循環等待鏈,鏈中每個進程已得到的資源同時被 鏈中下一個進程所請求。即存在一個處於等待狀態的進程集合{Pl, P2, …, pn},其中Pi等 待的資源被P(i+1)佔有(i=0, 1, …, n-1),Pn等待的資源被P0佔有,如圖2-15所示。

直觀上看,循環等待條件彷佛和死鎖的定義同樣,其實否則。按死鎖定義構成等待環所 要求的條件更嚴,它要求Pi等待的資源必須由P(i+1)來知足,而循環等待條件則無此限制。 例如,系統中有兩臺輸出設備,P0佔有一臺,PK佔有另外一臺,且K不屬於集合{0, 1, …, n}。

Pn等待一臺輸出設備,它能夠從P0得到,也可能從PK得到。所以,雖然Pn、P0和其餘 一些進程造成了循環等待圈,但PK不在圈內,若PK釋放了輸出設備,則可打破循環等待, 如圖2-16所示。所以循環等待只是死鎖的必要條件。

這裏寫圖片描述

資源分配圖含圈而系統又不必定有死鎖的緣由是同類資源數大於1。但若系統中每類資 源都只有一個資源,則資源分配圖含圈就變成了系統出現死鎖的充分必要條件。

以上這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不知足,就不會發生死鎖。

產生死鎖的一個例子:

/**
 * 一個簡單的死鎖類
 * 當DeadLock類的對象flag==1時(td1),先鎖定o1,睡眠500毫秒
 * 而td1在睡眠的時候另外一個flag==0的對象(td2)線程啓動,先鎖定o2,睡眠500毫秒
 * td1睡眠結束後須要鎖定o2才能繼續執行,而此時o2已被td2鎖定;
 * td2睡眠結束後須要鎖定o1才能繼續執行,而此時o1已被td1鎖定;
 * td一、td2相互等待,都須要獲得對方鎖定的資源才能繼續執行,從而死鎖。
 */
public class DeadLock implements Runnable {
    public int flag = 1;  
    //靜態對象是類的全部對象共享的  
    private static Object o1 = new Object(), o2 = new Object();  
    @Override  
    public void run() {  
        System.out.println("flag=" + flag);  
        if (flag == 1) {  
            synchronized (o1) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o2) {  
                    System.out.println("1");  
                }  
            }  
        }  
        if (flag == 0) {  
            synchronized (o2) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o1) {  
                    System.out.println("0");  
                }  
            }  
        }  
    }  

    public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;
        //td1,td2都處於可執行狀態,但JVM線程調度先執行哪一個線程是不肯定的。  
        //td2的run()可能在td1的run()以前運行  
        new Thread(td1).start();  
        new Thread(td2).start();
    }  
}

6、處理死鎖的方法

  • 預防死鎖:經過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或幾個條件,來防止死鎖的發生。
  • 避免死鎖:在資源的動態分配過程當中,用某種方法去防止系統進入不安全狀態,從而避免死鎖的發生。
  • 檢測死鎖:容許系統在運行過程當中發生死鎖,但可設置檢測機構及時檢測死鎖的發生,並採起適當措施加以清除。
  • 解除死鎖:當檢測出死鎖後,便採起適當措施將進程從死鎖狀態中解脫出來。

7、 預防死鎖

經過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或幾個條件,來防止死鎖的發生

破壞死鎖的條件:

  1. 破壞「互斥」條件:
    就是在系統裏取消互斥。若資源不被一個進程獨佔使用,那麼死鎖是確定不會發生的。但通常來講在所列的四個條件中,「互斥」條件是沒法破壞的。所以,在死鎖預防裏主要是破壞其餘幾個必要條件,而不去涉及破壞「互斥」條件。

    注意:互斥條件不能被破壞,不然會形成結果的不可再現性。

  2. 破壞「佔有並等待」條件:
    破壞「佔有並等待」條件,就是在系統中不容許進程在已得到某種資源的狀況下,申請其餘資源。即要想出一個辦法,阻止進程在持有資源的同時申請其餘資源。
    方法一:建立進程時,要求它申請所需的所有資源,系統或知足其全部要求,或什麼也不給它。這是所謂的 「 一次性分配」方案。
    方法二:要求每一個進程提出新的資源申請前,釋放它所佔有的資源。這樣,一個進程在須要資源S時,須先把它先前佔有的資源R釋放掉,而後才能提出對S的申請,即便它可能很快又要用到資源R。

  3. 破壞「不可搶佔」條件:
    破壞「不可搶佔」條件就是容許對資源實行搶奪。
    方法一:若是佔有某些資源的一個進程進行進一步資源請求被拒絕,則該進程必須釋放它最初佔有的資源,若是有必要,可再次請求這些資源和另外的資源。
    方法二:若是一個進程請求當前被另外一個進程佔有的一個資源,則操做系統能夠搶佔另外一個進程,要求它釋放資源。只有在任意兩個進程的優先級都不相同的條件下,方法二才能預防死鎖。

  4. 破壞「循環等待」條件:
    破壞「循環等待」條件的一種方法,是將系統中的全部資源統一編號,進程可在任什麼時候刻提出資源申請,但全部申請必須按照資源的編號順序(升序)提出。這樣作就能保證系統不出現死鎖。

銀行轉帳經典案例中的死鎖的避免

解決前面的銀行轉帳經典案例的死鎖, 有如下方法

方法一:破壞請求和保持條件

每一個櫃員均可以取放帳本,很容易出現互相等待的狀況。要想破壞請求和保持條件,就要一次性拿到全部資源。

能夠不容許櫃員均可以取放帳本,帳本要由單獨的帳本管理員來管理

在這裏插入圖片描述

也就是說帳本管理員拿取帳本是臨界區,若是隻拿到其中之一的帳本,那麼不會給櫃員,而是等待櫃員下一次詢問是否兩個帳本都在

//帳本管理員
public class AccountBookManager {
    synchronized boolean getAllRequiredAccountBook( Object from, Object to){
        if(拿到全部帳本){
            return true;
        } else{
            return false;
        }
    }
    // 歸還資源
    synchronized void releaseObtainedAccountBook(Object from, Object to){
        歸還獲取到的帳本
    }
}
 
 
 
 
public class Account {
    //單例的帳本管理員
    private AccountBookManager accountBookManager;
 
 
    public void transfer(Account target, int amt){
        // 一次性申請轉出帳戶和轉入帳戶,直到成功
        while(!accountBookManager.getAllRequiredAccountBook(this, target)){
            return;
        }
 
 
        try{
            // 鎖定轉出帳戶
            synchronized(this){
                // 鎖定轉入帳戶
                synchronized(target){
                    if (this.balance > amt){
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            accountBookManager.releaseObtainedAccountBook(this, target);
        }
    }
}

方法二:破壞不可剝奪條件

上面已經給了你小小的提示,爲了解決內置鎖的執着,Java 顯示鎖支持通知(notify/notifyall)和等待(wait),也就是說該功能能夠實現喊一嗓子 老鐵,鐵蛋兒的帳本先給我用一下,用完還給你 的功能,

還有,能夠經過 加鎖時限(線程嘗試獲取鎖的時候加上必定的時限,超過期限則放棄對該鎖的請求,並釋放本身佔有的鎖)去解決。

下面是一個相似的例子。

在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功得到全部須要的鎖,則會進行回退並釋放全部已經得到的鎖,而後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,而且讓該應用在沒有得到鎖的時候能夠繼續運行(譯者注:加鎖超時後能夠先繼續運行乾點其它事情,再回頭來重複以前加鎖的邏輯)。

如下是一個例子,展現了兩個線程以不一樣的順序嘗試獲取相同的兩個鎖,在發生超時後回退並重試的場景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1’s lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2’s lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,線程2比線程1早200毫秒進行重試加鎖,所以它能夠先成功地獲取到兩個鎖。這時,線程1嘗試獲取鎖A而且處於等待狀態。當線程2結束時,線程1也能夠順利的得到這兩個鎖(除非線程2或者其它線程在線程1成功得到兩個鎖以前又得到其中的一些鎖)。

須要注意的是,因爲存在鎖的超時,因此咱們不能認爲這種場景就必定是出現了死鎖。也多是由於得到了鎖的線程(致使其它線程超時)須要很長的時間去完成它的任務。

此外,若是有很是多的線程同一時間去競爭同一批資源,就算有超時和回退機制,仍是可能會致使這些線程重複地嘗試但卻始終得不到鎖。若是隻有兩個線程,而且重試的超時時間設定爲0到500毫秒之間,這種現象可能不會發生,可是若是是10個或20個線程狀況就不一樣了。由於這些線程等待相等的重試時間的機率就高的多(或者很是接近以致於會出現問題)。
(譯者注:超時和重試機制是爲了不在同一時間出現的競爭,可是當線程不少時,其中兩個或多個線程的超時時間同樣或者接近的可能性就會很大,所以就算出現競爭而致使超時後,因爲超時時間同樣,它們又會同時開始重試,致使新一輪的競爭,帶來了新的問題。)

這種機制存在一個問題,在Java中不能對synchronized同步塊設置超時時間。須要使用 JUC 的顯式鎖。

方法三:破壞環路等待條件

破壞環路等待條件,就是按照相同的順序得到鎖。

若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。看下面這個例子:

Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
wait for A
wait for B
wait for C

若是一個線程(好比線程3)須要一些鎖,那麼它必須按照肯定的順序獲取鎖。它只有得到了從順序上排在前面的鎖以後,才能獲取後面的鎖。

例如,線程2和線程3只有在獲取了鎖A以後才能嘗試獲取鎖C(譯者注:獲取鎖A是獲取鎖C的必要條件)。由於線程1已經擁有了鎖A,因此線程2和3須要一直等到鎖A被釋放。而後在它們嘗試對B或C加鎖以前,必須成功地對A加了鎖。

按照順序加鎖是一種有效的死鎖預防機制。可是,這種方式須要你事先知道全部可能會用到的鎖(譯者注:並對這些鎖作適當的排序),但總有些時候是沒法預知的。

破壞環路等待條件也很簡單,咱們只須要將資源序號大小排序獲取就會解決這個問題,將環路拆除

在這裏插入圖片描述

按照id大小的順序來加鎖,先鎖住id 小的,而後才鎖住 id 大的

class Account {
  private int id;
  private int balance;
  // 轉帳
  void transfer(Account target, int amt){
    Account smaller = this        
    Account larger = target;    
    // 排序
    if (this.id > target.id) { 
      smaller = target;           
      larger = this;            
    }                          
    // 鎖定序號小的帳戶
    synchronized(smaller){
      // 鎖定序號大的帳戶
      synchronized(larger){ 
        if (this.balance > amt){
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

當 smaller 被佔用時,其餘線程就會被阻塞,也就不會存在死鎖了.

八 避免死鎖

理解了死鎖的緣由,尤爲是產生死鎖的四個必要條件,就能夠最大可能地避免、預防和解除死鎖。因此,在系統設計、進程調度等方面注意如何讓這四個必要條件不成立,如何肯定資源的合理分配算法,避免進程永久佔據系統資源。此外,也要防止進程在處於等待狀態的狀況下佔用資源。所以,對資源的分配要給予合理的規劃。

預防死鎖和避免死鎖的區別:
預防死鎖是設法至少破壞產生死鎖的四個必要條件之一,嚴格的防止死鎖的出現,而避免死鎖則不那麼嚴格的限制產生死鎖的必要條件的存在,由於即便死鎖的必要條件存在,也不必定發生死鎖。避免死鎖是在系統運行過程當中注意避免死鎖的最終發生。

經常使用避免死鎖的方法

有序資源分配法

這種算法資源按某種規則系統中的全部資源統一編號(例如打印機爲一、磁帶機爲二、磁盤爲三、等等),申請時必須以上升的次序。系統要求申請進程:
  一、對它所必須使用的並且屬於同一類的全部資源,必須一次申請完;
  二、在申請不一樣類資源時,必須按各種設備的編號依次申請。例如:進程PA,使用資源的順序是R1,R2; 進程PB,使用資源的順序是R2,R1;若採用動態分配有可能造成環路條件,形成死鎖。
  採用有序資源分配法:R1的編號爲1,R2的編號爲2;
  PA:申請次序應是:R1,R2
  PB:申請次序應是:R1,R2
  這樣就破壞了環路條件,避免了死鎖的發生。
  

銀行家算法

詳見銀行家算法.

九 檢測死鎖

死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖而且鎖超時也不可行的場景。

每當一個線程得到了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此以外,每當有線程請求鎖,也須要記錄在這個數據結構中。

當一個線程請求鎖失敗時,這個線程能夠遍歷鎖的關係圖看看是否有死鎖發生。例如,線程A請求鎖7,可是鎖7這個時候被線程B持有,這時線程A就能夠檢查一下線程B是否已經請求了線程A當前所持有的鎖。若是線程B確實有這樣的請求,那麼就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。

固然,死鎖通常要比兩個線程互相持有對方的鎖這種狀況要複雜的多。線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。線程A爲了檢測死鎖,它須要遞進地檢測全部被B請求的鎖。從線程B所請求的鎖開始,線程A找到了線程C,而後又找到了線程D,發現線程D請求的鎖被線程A本身持有着。這是它就知道發生了死鎖。

下面是一幅關於四個線程(A,B,C和D)之間鎖佔有和請求的關係圖。像這樣的數據結構就能夠被用來檢測死鎖。

這裏寫圖片描述

通常來講,因爲操做系統有併發,共享以及隨機性等特色,經過預防和避免的手段達到排除死鎖的目的是很困難的。這須要較大的系統開銷,並且不能充分利用資源。爲此,一種簡便的方法是系統爲進程分配資源時,不採起任何限制性措施,可是提供了檢測和解脫死鎖的手段:能發現死鎖並從死鎖狀態中恢復出來。所以,在實際的操做系統中每每採用死鎖的檢測與恢復方法來排除死鎖。
死鎖檢測與恢復是指系統設有專門的機構,當死鎖發生時,該機構可以檢測到死鎖發生的位置和緣由,並能經過外力破壞死鎖發生的必要條件,從而使得併發進程從死鎖狀態中恢復出來。
這時進程P1佔有資源R1而申請資源R2,進程P2佔有資源R2而申請資源R1,按循環等待條件,進程和資源造成了環路,因此係統是死鎖狀態。進程P1,P2是參與死鎖的進程。
下面咱們再來看一看死鎖檢測算法。算法使用的數據結構是以下這些:
佔有矩陣A:nm階,其中n表示併發進程的個數,m表示系統的各種資源的個數,這個矩陣記錄了每個進程當前佔有各個資源類中資源的個數。
申請矩陣R:n
m階,其中n表示併發進程的個數,m表示系統的各種資源的個數,這個矩陣記錄了每個進程當前要完成工做須要申請的各個資源類中資源的個數。
空閒向量T:記錄當前m個資源類中空閒資源的個數。
完成向量F:布爾型向量值爲真(true)或假(false),記錄當前n個併發進程可否進行完。爲真即能進行完,爲假則不能進行完。
臨時向量W:開始時W:=T。
算法步驟:
(1)W:=T,
對於全部的i=1,2,…,n,
若是A[i]=0,則F[i]:=true;不然,F[i]:=false
(2)找知足下面條件的下標i:
F[i]:=false而且R[i]〈=W
若是不存在知足上面的條件i,則轉到步驟(4)。
(3)W:=W+A[i]
F[i]:=true
轉到步驟(2)
(4)若是存在i,F[i]:=false,則系統處於死鎖狀態,且Pi進程參與了死鎖。何時進行死鎖的檢測取決於死鎖發生的頻率。若是死鎖發生的頻率高,那麼死鎖檢測的頻率也要相應提升,這樣一方面能夠提升系統資源的利用率,一方面能夠避免更多的進程捲入死鎖。若是進程申請資源不能知足就馬上進行檢測,那麼每當死鎖造成時即能被發現,這和死鎖避免的算法相近,只是系統的開銷較大。爲了減少死鎖檢測帶來的系統開銷,通常採起每隔一段時間進行一次死鎖檢測,或者在CPU的利用率下降到某一數值時,進行死鎖的檢測。

十 解除死鎖

一旦檢測出死鎖,就應當即釆取相應的措施,以解除死鎖。
死鎖解除的主要方法有:

  1. 資源剝奪法。掛起某些死鎖進程,並搶佔它的資源,將這些資源分配給其餘的死鎖進程。但應防止被掛起的進程長時間得不到資源,而處於資源匱乏的狀態。
  2. 撤銷進程法。強制撤銷部分、甚至所有死鎖進程並剝奪這些進程的資源。撤銷的原則能夠按進程優先級和撤銷進程代價的高低進行。
  3. 進程回退法。讓一(多)個進程回退到足以迴避死鎖的地步,進程回退時自願釋放資源而不是被剝奪。要求系統保持進程的歷史信息,設置還原點。

參考文檔:

https://cloud.tencent.com/developer/article/1541513

https://blog.csdn.net/fdoubleman/article/details/97238420

http://www.voidcn.com/article/p-gjtwwpmp-ws.html

http://www.javashuo.com/article/p-nsfkkato-eh.html

相關文章
相關標籤/搜索