1. 程式人生 > >競態與死鎖的理解

競態與死鎖的理解

1、競態條件:

  • 定義:競態條件指的是一種特殊的情況,在這種情況下各個執行單元以一種沒有邏輯的順序執行動作,從而導致意想不到的結果。
  • 舉例1:執行緒T修改資源R後,釋放了它對R的寫訪問權,之後又重新奪回R的讀訪問權再使用它,並以為它的狀態仍然保持在它釋放它之後的狀態。但是在寫訪問權釋放後到重新奪回讀訪問權的這段時間間隔中,可能另一個執行緒已經修改了R的狀態。(寫——讀之間,該變數已經被其他執行緒修改
  • 舉例2:另一個經典的競態條件的例子就是生產者/消費者模型。生產者通常使用同一個實體記憶體空間儲存被生產的資訊。一般說來,我們不會忘記在生產者與消費者的併發訪問之間保護這個空間。容易被我們忘記的是生產者必須確保在生產新資訊前,舊的資訊已被消費者所讀取。如果我們沒有采取相應的預防措施,我們將面臨生產的資訊從未被消費的危險
    。(生產者生產出來的資訊,消費者未消費
  • 危害漏洞:如果靜態條件沒有被妥善的管理,將導致安全系統的漏洞。同一個應用程式的另一個例項很可能會引發一系列開發者所預計不到的事件。一般來說,必須對那種用於確認身份鑑別結果的布林量的寫訪問做最完善的保護。如果沒有這麼做,那麼在它的狀態被身份鑑別機制設定後,到它被讀取以保護對資源的訪問的這段時間內,很有可能已經被修改了。已知的安全漏洞很多都歸咎於對靜態條件不恰當的管理。其中之一甚至影響了Unix作業系統的核心。

2、死鎖:

(1)死鎖介紹

  • 定義:死鎖指的是由於兩個或多個執行單元之間相互等待對方結束而引起阻塞的情況。每個執行緒都擁有其他執行緒所需要的資源,同時又等待其他執行緒已經擁有的資源,並且每個執行緒在獲取所有需要資源之前都不會釋放自己已經擁有的資源。
  • 舉例1:一個執行緒T1獲得了對資源R1的訪問權,一個執行緒T2獲得了對資源R2的訪問權,T1請求對R2的訪問權但是由於此權力被T2所佔而不得不等待,T2請求對R1的訪問權但是由於此權力被T1所佔而不得不等待。T1和T2將永遠維持等待狀態,此時我們陷入了死鎖的處境!這種問題比你所遇到的大多數的bug都要隱祕,
  • 舉例2:打電話雙方,互相撥號,同時佔用通訊通道,互相等待。
  • 幾種解決方案
  • 1.在同一時刻不允許一個執行緒訪問多個資源。避免一個執行緒在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。
  • 2.為資源訪問權的獲取定義一個關係順序。換句話說,當一個執行緒已經獲得了R1的訪問權後,將無法獲得R2的訪問權。當然,訪問權的釋放
    必須遵循相反的順序。
  • 3.為所有訪問資源的請求系統地定義一個最大等待時間(超時時間),並妥善處理請求失敗的情況。嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
  • 4.對於資料庫鎖,加鎖和解鎖必須在一個數據庫連線裡,否則會出現解鎖失敗的情況。
  • 5.避免同一個執行緒同時獲取多個鎖。
  • 分析:前兩種技術效率更高但是也更加難於實現。事實上,它們都需要很強的約束,而這點隨著應用程式的演變將越來越難以維護。儘管如此,使用這些技術不會存在失敗的情況。

大的專案通常使用第三種方法。事實上,如果專案很大,一般來說它會使用大量的資源。在這種情況下,資源之間發生衝突的概率很低,也就意味著失敗的情況會比較罕見。我們認為這是一種樂觀的方法。

(2)死鎖的情形

  • 1.資料庫系統的設計:檢測到一組事務發生死鎖時(通過在表示等待關係的有向圖中搜索迴圈),將選擇一個犧牲者並放棄這個事務,作為犧牲者的事務會釋放自己持有的資源,從而使得其他事務能夠繼續進行。後面應用程式可以重新執行被強制中止的事務。
  • 2.JVM發生死鎖的時候,發生死鎖的執行緒就永遠不能再使用,只能中止並重啟引用程式。

2、死鎖分類(鎖順序死鎖、資源死鎖)

(1)鎖順序死鎖:

1.鎖順序死鎖出現的原因:使用加鎖機制來確保執行緒安全,但是過渡的使用加鎖。比如,執行緒A先鎖住left,後嘗試鎖住right,而執行緒B先鎖住right,後嘗試鎖住left,則A和B都各自擁有資源left和right,互相等待對方的資源,所以永久等待。

2.鎖順序死鎖分析

  • 兩個執行緒試圖以不同的順序來獲取相同的鎖,就會發生死鎖。如果按照相同的順序來請求鎖,就不會出現迴圈的加鎖依賴性,因此就不會出現死鎖。
  • 如果所有的執行緒都以相同的順序來獲取鎖,程式不會出現順序性死鎖,可以通過定義獲得鎖的順序來避免死鎖。
  • 如果在持有鎖的情況下呼叫某個外部的方法,那麼就需要警惕活躍性問題,因為在這個外部方法中可能會獲取其他的鎖(可能產生死鎖),或者阻塞時間過長,導致其他的執行緒無法及時獲得當前被持有的鎖。

3.解決方法:開放呼叫,在呼叫外部某個方法時,不需要持有鎖。

(2)資源死鎖:

1.資源死鎖出現的原因:使用執行緒池和訊號量限制對資源的使用。

2.資源死鎖的情形(資源池,資料庫連線池中):

  • 正如當多個執行緒互相持有彼此正在等待的鎖而又不釋放自己已持有鎖時發生死鎖,當多個執行緒在相同資源集合上等待時,也可能會發生死鎖。如,執行緒A持有資料庫C的連線,並等待資料庫D的連線;而執行緒B持有資料庫D的連線,並等待與資料庫C的連線。(資源池越大,出現該情況概率越小)
  • 另一種形式的資源死鎖是執行緒飢餓死鎖,一個任務提交另一個任務,並等待被提交恩物在單執行緒中的執行完成,第一個任務就一直等待,並使得另個任務以及這個Executor中執行的其他任務都停止執行。如果某些任務需要等待其他任務的結果,那麼這些任務往往是產生飢餓死鎖的主要矛盾,有界執行緒池/資源池與互相依賴的任務不能一起使用。

3.解決:有界執行緒池/資源池與互相依賴的任務不能一起使用。

3、死鎖的避免與診斷

(1)支援定時的鎖:

顯式的使用Lock類中的定時tryLock功能來代替內建鎖機制(使用內建鎖時,只要沒有獲得鎖,就會永遠等待),而顯式鎖會指定一個超時時限,在等待超過該時間後,tryLock會返回一個失敗資訊。如果不能獲得所需要的鎖,定時的鎖或者輪詢鎖會釋放已經得到的鎖,然後重新嘗試獲得所有的鎖。(失敗的記錄會被記錄到日誌中,並採取措施),這樣技術只有在同時獲得兩個鎖時才有效,如果在巢狀的方法中請求多個鎖,那麼即使你知道已經持有了外層鎖,也無法釋放它。

(2)通過執行緒轉儲資訊來分析死鎖:

JVM可以通過執行緒轉儲來幫助識別死鎖的發生。執行緒的轉儲包括各個執行中的執行緒的棧追蹤資訊,同時包含加鎖資訊,例如每個執行緒持有哪些鎖,在哪些棧幀中獲得這些鎖,以及被阻塞的執行緒正在等待獲取那個鎖,在生成執行緒轉儲資訊之前,JVM將在等待關係圖中通過搜尋迴圈來找死鎖。如果發現一個死鎖,則獲取相應的死鎖資訊,如在死鎖中涉及哪些鎖和執行緒,以及這個鎖的獲取操作位於程式的哪些位置。

4、其他的活躍性危險:

(1)飢餓

1.飢餓出現原因:當前執行緒由於無法訪問所需要的資源而不能繼續執行的時候,就發生飢餓,引發飢餓最常見的資源就是CPU時鐘週期。還有就是Java程式中對執行緒的優先順序使用不當,或者某個執行緒持有鎖時,無限制的執行一些無法結束的結構(無限迴圈或者無限制地等待某個資源),導致其他需要獲得該鎖的執行緒無法獲取它而導致飢餓。

2.解決方案:要避免使用執行緒優先順序,因為改變執行緒的優先順序會增加平臺依賴性,並導致活躍性問題。使用預設的優先順序就行了。

(2)糟糕的響應性

不良的鎖管理會導致糟糕的響應性;在GUI應用程式中使用後臺執行緒會導致糟糕的響應性,後臺執行緒會與事件執行緒共同競爭CPU的時鐘週期。。

(3)活鎖

1.活鎖出現的原因1:該問題儘管不會阻塞執行緒,但是不能繼續執行,因為執行緒總是不斷重複執行相同的操作,而且總失敗,通常發生在處理事務訊息的應用程式中。

舉例1:處理事務訊息的引用程式不能成功處理某個訊息,訊息處理機制就回滾整個事務,並將它重新放到佇列的開頭,訊息處理器在處理這個訊息時存在錯誤並導致失敗,然後每次這個訊息都從佇列中取出並傳遞到存在錯誤的處理器時,就會發生事務的回滾,處理器反覆被呼叫並返回相同的結果(毒藥訊息)。處理器並沒有被阻塞,但是無法繼續執行下去。(過度的錯誤恢復程式碼,不可修復的錯誤作為可修復的錯誤)。

2.活鎖出現的原因2:當多個相互協作的執行緒都對彼此進行響應從而修改各自的狀態,並使得任何一個執行緒都無法繼續執行下去,就發生活鎖。

舉例2:兩個過於禮貌的人在半路上面對面相遇,彼此都讓出對方的路,然後在另一條路上又相遇了,反覆避讓下去。

3.解決方案:要解決活鎖問題,需要在重試機制中引入隨機性。如網路中兩臺機器需要用相同載波傳送資料包,資料包傳送衝突,然後過段時間重發又發生衝突,並不斷衝突下去,如果使用隨機指數退避演算法,就不會發生衝突。

在併發應用中,通過等待隨機長度的時間和回退可以有效地避免活鎖的發生。

5、死鎖的條件

1.死鎖多個程序或執行緒競爭某一共享資源,而出現的一種互相等待的現象

2.產生死鎖的主要原因:1 系統資源不夠 2 程序或者執行緒執行推進的順序不合適3 資源分配不當

3.死鎖的四個必要條件

  • 互斥條件:一個資源每次只能被一個程序使用。(一個資源只被一個程序使用)任務使用的資源至少有一個是不能共享的。
  • 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不變。(自己的資源不釋放,等待所需要的其他資源)至少有一個任務它必須持有一個資源且正在等待獲取一個當前被別的任務持有的資源。
  • 不剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪。(自己的資源,使用完前不被剝奪)資源不能被任務搶佔,任務必須把資源釋放當做普通事件。
  • 迴圈等待條件:若干程序之間形成的一種頭尾相接的迴圈等待資源關係。(程序之間迴圈等待資源)必須有迴圈等待,這時,一個任務等待其他任務所持有的資源,後者又在等待另一個任務所持有的資源,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,大家都被鎖住。

只要上述條件之一不滿足,就不會產生死鎖。防止死鎖最容易的是破壞第4個條件,只要改變等待資源的順序即可。

6、預防死鎖

防止死鎖的發生只需破壞死鎖產生的四個必要條件之一即可。

1) 破壞互斥條件

  • 如果允許系統資源都能共享使用,則系統不會進入死鎖狀態。但有些資源根本不能同時訪問,如印表機等臨界資源只能互斥使用。所以,破壞互斥條件而預防死鎖的方法不太可行,而且在有的場合應該保護這種互斥性。

2) 破壞不剝奪條件

  • 當一個已保持了某些不可剝奪資源的程序,請求新的資源而得不到滿足時,它必須釋放已經保持的所有資源,待以後需要時再重新申請。這意味著,一個程序已佔有的資源會被暫時釋放,或者說是被剝奪了,或從而破壞了不可剝奪條件。
  • 該策略實現起來比較複雜,釋放已獲得的資源可能造成前一階段工作的失效,反覆地申請和釋放資源會增加系統開銷,降低系統吞吐量。這種方法常用於狀態易於儲存和恢復的資源,如CPU的暫存器及記憶體資源,一般不能用於印表機之類的資源。

3) 破壞請求和保持條件

  • 釆用預先靜態分配方法,即程序在執行前一次申請完它所需要的全部資源,在它的資源未滿足前,不把它投入執行。一旦投入執行後,這些資源就一直歸它所有,也不再提出其他資源請求,這樣就可以保證系統不會發生死鎖。
  • 這種方式實現簡單,但缺點也顯而易見,系統資源被嚴重浪費,其中有些資源可能僅在執行初期或執行快結束時才使用,甚至根本不使用。而且還會導致“飢餓”現象,當由於個別資源長期被其他程序佔用時,將致使等待該資源的程序遲遲不能開始執行。

4) 破壞迴圈等待條件

  • 為了破壞迴圈等待條件,可釆用順序資源分配法。首先給系統中的資源編號,規定每個程序,必須按編號遞增的順序請求資源,同類資源一次申請完。也就是說,只要程序提出申請分配資源Ri,則該程序在以後的資源申請中,只能申請編號大於Ri的資源。
  • 這種方法存在的問題是,編號必須相對穩定,這就限制了新型別裝置的增加;儘管在為資源編號時已考慮到大多數作業實際使用這些資源的順序,但也經常會發生作業使用資源的順序與系統規定順序不同的情況,造成資源的浪費;此外,這種按規定次序申請資源的方法,也必然會給使用者的程式設計帶來麻煩。

 上面我們講到的死鎖預防是排除死鎖的靜態策略,它使產生死鎖的四個必要條件不能同時具備,從而對程序申請資源的活動加以限制,以保證死鎖不會發生。下面我們介紹排除死鎖的動態策略--死鎖的避免,它不限制程序有關申請資源的命令,而是對程序所發出的每一個申請資源命令加以動態地檢查,並根據檢查結果決定是否進行資源分配。就是說,在資源分配過程中若預測有發生死鎖的可能性,則加以避免。這種方法的關鍵是確定資源分配的安全性。

7、安全序列

  我們首先引入安全序列的定義:所謂系統是安全的,是指系統中的所有程序能夠按照某一種次序分配資源,並且依次地執行完畢,這種程序序列{P1,P2,...,Pn}就是安全序列。如果存在這樣一個安全序列,則系統是安全的;如果系統不存在這樣一個安全序列,則系統是不安全的。

  安全序列{P1,P2,...,Pn}是這樣組成的:若對於每一個程序Pi,它需要的附加資源可以被系統中當前可用資源加上所有程序Pj當前佔有資源之和所滿足,則{P1,P2,...,Pn}為一個安全序列,這時系統處於安全狀態,不會進入死鎖狀態。

  雖然存在安全序列時一定不會有死鎖發生,但是系統進入不安全狀態(四個死鎖的必要條件同時發生)也未必會產生死鎖。當然,產生死鎖後,系統一定處於不安全狀態。 

8、銀行家演算法

  這是一個著名的避免死鎖的演算法,是由Dijstra首先提出來並加以解決的。

(1)背景知識

  一個銀行家如何將一定數目的資金安全地借給若干個客戶,使這些客戶既能借到錢完成要乾的事,同時銀行家又能收回全部資金而不至於破產,這就是銀行家問題。這個問題同作業系統中資源分配問題十分相似:銀行家就像一個作業系統,客戶就像執行的程序,銀行家的資金就是系統的資源。

(2)問題的描述

  一個銀行家擁有一定數量的資金,有若干個客戶要貸款。每個客戶須在一開始就宣告他所需貸款的總額。若該客戶貸款總額不超過銀行家的資金總數,銀行家可以接收客戶的要求。客戶貸款是以每次一個資金單位(如1萬RMB等)的方式進行的,客戶在借滿所需的全部單位款額之前可能會等待,但銀行家須保證這種等待是有限的,可完成的。

  例如:有三個客戶C1,C2,C3,向銀行家借款,該銀行家的資金總額為10個資金單位,其中C1客戶要借9各資金單位,C2客戶要借3個資金單位,C3客戶要借8個資金單位,總計20個資金單位。某一時刻的狀態如圖所示。

C1 2(7)

C2 2(1)

C3 4(4)

餘額2

C1 2(7)

C3 4(4)

餘額4

C1 2(7)

餘額8

餘額10

    (a)

     (b)

     (c)

     (d)

銀行家演算法示意圖

  對於a圖的狀態,按照安全序列的要求,我們選的第一個客戶應滿足該客戶所需的貸款小於等於銀行家當前所剩餘的錢款,可以看出只有C2客戶能被滿足:C2客戶需1個資金單位,小銀行家手中的2個資金單位,於是銀行家把1個資金單位借給C2客戶,使之完成工作並歸還所借的3個資金單位的錢,進入b圖。同理,銀行家把4個資金單位借給C3客戶,使其完成工作,在c圖中,只剩一個客戶C1,它需7個資金單位,這時銀行家有8個資金單位,所以C1也能順利借到錢並完成工作。最後(見圖d)銀行家收回全部10個資金單位,保證不賠本。那麼客戶序列{C1,C2,C3}就是個安全序列(C2、C3、C1),按照這個序列貸款,銀行家才是安全的。否則的話,若在圖b狀態時,銀行家把手中的4個資金單位借給了C1,則出現不安全狀態:這時C1,C3均不能完成工作,而銀行家手中又沒有錢了,系統陷入僵持局面,銀行家也不能收回投資。

  綜上所述,銀行家演算法是從當前狀態出發,逐個按安全序列檢查各客戶誰能完成其工作,然後假定其完成工作且歸還全部貸款,再進而檢查下一個能完成工作的客戶,......。如果所有客戶都能完成工作,則找到一個安全序列,銀行家才是安全的。

  從上面分析看出,銀行家演算法允許死鎖必要條件中的互斥條件,佔有且申請條件,不可搶佔條件的存在,這樣,它與預防死鎖的幾種方法相比較,限制條件少了,資源利用程度提高了。

這是該演算法的優點。其缺點是:

   〈1〉這個演算法要求客戶數保持固定不變,這在多道程式系統中是難以做到的。   

   〈2〉這個演算法保證所有客戶在有限的時間內得到滿足,但實時客戶要求快速響應,所以要考慮這個因素。  

   〈3〉由於要尋找一個安全序列,實際上增加了系統的開銷

9、死鎖的檢測和解除

先前的死鎖預防以及避免演算法都是在程序分配資源時施加限制條件或進行檢測,若系統為程序分配資源時不採取任何措施,就應該提供死鎖檢測和解除的手段。

(1)資源分配圖

系統死鎖,可利用資源分配圖來描述。如圖2-17所示,用圓圈代表一個程序,用框代表一類資源。由於一種型別的資源可能有多個,用框中的一個點代表一類資源中的一個資源。從程序到資源的有向邊叫請求邊,表示該程序申請一個單位的該類資源;從資源到程序的邊叫分配邊,表示該類資源已經有一個資源被分配給了該程序。

 

在圖2-17所示的資源分配圖中,程序P1已經分得了兩個R1資源,並又請求一個R2 資源;程序P2分得了一個R1和一個R2資源,並又請求一個R1資源。

(2)死鎖定理

通過簡化資源分配圖的方法檢測系統狀態S是否為死鎖狀態。簡化步驟如下:

 

  • 1) 在資源分配圖中,找出既不阻塞又不是孤點的程序Pi(即找出一條有向邊與它相連,且該有向邊對應資源的申請數量小於等於系統中已有空閒資源數量。若所有的連線該程序的邊均滿足上述條件,則這個程序能繼續執行直至完成,然後釋放它所佔有的所有資源)。消去它所有的請求邊和分配邊,使之成為孤立的結點。在圖2-18(a)中,P1是滿足這一條件的程序結點,將P1的所有邊消去,便得到圖(b)所示的情況。
  • 2) 程序Pi所釋放的資源,可以喚醒某些因等待這些資源而阻塞的程序,原來的阻塞程序可能變為非阻塞程序。如圖2-18(c)所示。
  • S為死鎖的條件是當且僅當S狀態的資源分配圖是不可完全簡化的,該條件為死鎖定理。

(3)死鎖的解除

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

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

(4)避免死鎖的方法

1)避免一個執行緒同時獲取多個鎖;

2)避免一個執行緒在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源;

3)嘗試使用定時鎖,使用lock.tryLock(timeout)來代替使用內部鎖機制;

4)對於資料庫鎖,加鎖和解鎖必須在一個數據庫連線裡,否則會出現解鎖失敗的情況。

10、死鎖程式碼實現步驟

1)兩個執行緒裡面分別持有兩個Object物件:lock1和lock2,這兩個lock作為同步程式碼塊的鎖;

2)執行緒1的run()方法中同步程式碼快先獲取lock1的物件所,Thread.sleep(),時間不需要太多,50毫秒差不多;然後接著獲取lock2的物件鎖。(sleep可以防止執行緒1啟動一下子連續獲得lock1和lock2兩個物件的物件鎖;

3)執行緒2的run()方法中同步程式碼塊先獲取lock2的物件鎖,接著獲取lock1的物件鎖,這時lock1的物件鎖已經被執行緒1鎖持有,執行緒2肯定是要等待執行緒1釋放lock1的物件鎖的;

4)執行緒1睡完後,執行緒2已經獲取lock2的物件鎖,執行緒1此時嘗試獲取lock2的物件鎖便被阻塞,而執行緒2準備獲取執行緒1的物件鎖時,也被阻塞。