死鎖是指一到多個線程阻塞等待的鎖被其餘線程持有且不釋放.當多個線程在同一時間按不一樣的順序來獲取相同的鎖的狀況下,會發生死鎖.html
例如,線程1持有鎖A且嘗試去獲取鎖B,而線程2持有鎖B且嘗試去獲取鎖A,那麼死鎖將會發生.線程1永遠獲取不到鎖B, 而線程2則永遠獲取不到鎖A.且它們都不知道死鎖已經發生.它們將永遠各自阻塞且持有鎖A和B.這種狀況咱們稱之爲死鎖.java
以下描述死鎖:sql
線程 1 持有鎖A, 嘗試獲取鎖B.
線程 2 持有鎖B, 嘗試獲取鎖A.
複製代碼
如下是一個死鎖的示例,在TreeNodes中調用不一樣實例的同步方法:數據庫
public class TreeNode {
TreeNode parent = null;
List children = new ArrayList();
public synchronized void addChild(TreeNode child){
if(!this.children.contains(child)) {
this.children.add(child);
child.setParentOnly(this);
}
}
public synchronized void addChildOnly(TreeNode child){
if(!this.children.contains(child){
this.children.add(child);
}
}
public synchronized void setParent(TreeNode parent){
this.parent = parent;
parent.addChildOnly(this);
}
public synchronized void setParentOnly(TreeNode parent){
this.parent = parent;
}
}
複製代碼
當線程1調用addChild
方式時,線程2在同一時間調用setParent
方法,且使用同一個parent和child實例,此時死鎖發生了.數據結構
以下描述:併發
// Thread 1:
// 此時持有parent對象鎖
parent.addChild(child);
// 嘗試去得到child對象鎖
child.setParentOnly(parent);
// Thread 2:
// 此時持有child對象鎖
child.setParent(parent);
// 嘗試去得到parent鎖
parent.addChildOnly(child);
複製代碼
當線程1成功調用parent.addChild(child)時,線程1已經進入同步代碼塊,併成功獲取parent對象鎖.其餘想要獲取parent對象鎖的線程只能等待線程1釋放.post
一樣的,當線程2成功調用child.setParent(parent)時,線程2已經進入同步代碼塊,併成功獲取child對象鎖,其餘想要獲取child對象鎖的線程只能等待線程2釋放.this
如今child和parent對象鎖同時被不一樣的兩個線程持有.當線程1嘗試去調用child.setParentOnly(parent)時,此時child對象鎖被線程2持有且未釋放,線程1只能阻塞等待child對象鎖.一樣的,線程2嘗試去調用parent.addChildOnly(child)時,此時parent對象鎖被線程1持有且未釋放,線程2只能阻塞等待parent對象鎖.如今兩個線程都在阻塞等待對方所持有的對象鎖釋放.spa
注意:以上死鎖發生須要兩個線程同時調用parent.addChild(child)和child.setParent(parent)且須要使用相同的parent和child對象實例.上文中說起的代碼可能須要執行不少次才能讓死鎖發生.線程
兩個線程須要在相同的時間點持有對方所須要的鎖.但凡其中一個線程領先另外一個線程一點,就能成功得到parent和child對象鎖,這樣另外一個線程一開始就只能阻塞等待對方釋放鎖.這樣死鎖就不會發生了.因爲線程的執行時機不可預測,所以咱們沒法按照預期來重現死鎖,只能說可能會發生死鎖.
死鎖能夠在多於兩個線程的狀況下產生.這可能比較難觀察.以下示例四個線程的死鎖狀況:
線程 1 持有鎖A, 等待鎖B
線程 2 持有鎖B, 等待鎖C
線程 3 持有鎖C, 等待鎖D
線程 4 持有鎖D, 等待鎖A
複製代碼
線程1等待線程2釋放鎖,線程2等待線程3釋放鎖,線程3等待線程4釋放鎖,線程4等待線程1釋放鎖.
數據庫事務是一個更加完整的死鎖狀況.一個事務中可能會有多個sql更新請求.當一條記錄被一個事務更新時會被當前事務鎖住,其餘想更新同一條記錄的事務只能等待持有當前行級鎖的事務釋放.同一個事務中的多個更新請求可能會鎖住數據庫中的多條記錄.
若是多個事務在同一時間進行且須要更新相同的記錄.那麼將會有發生死鎖的風險.
以下所示:
事務1, 更新請求1, 鎖住記錄1且進行更新
事務2, 更新請求1, 鎖住記錄2且進行更新
事務1, 更新請求2, 嘗試獲取記錄2的行級鎖更新記錄
事務2, 更新請求2, 嘗試獲取記錄1的行級鎖更新記錄
複製代碼
當記錄被不一樣的更新請求持有,且當前事務不能提早知道執行完當前事務所須要的所有行級鎖.那麼將很難在數據庫事務中檢查和預防死鎖.
在如下三種措施可以用來預防死鎖.
咱們知道死鎖會發生在多個線程以不一樣順序獲取相同鎖的狀況下.
若是咱們能確保全部線程都能以相同的順序來獲取鎖,那麼死鎖將不會發生.
以下所示:
線程1:
持有鎖A
持有鎖B
線程2:
等待獲取鎖A
當成功獲取鎖A時,獲取鎖C
線程3:
等待獲取鎖A
等待獲取鎖B
等待獲取鎖C
複製代碼
若是一個像線程3的線程須要獲取多個鎖,那麼線程須要按照給定的順序來獲取它們.當須要獲取序列中的後一個鎖以前只能先持有前一個鎖.
不管是線程2仍是3都須要先獲取到鎖A才能繼續獲取鎖C.當線程1已經持有鎖A時,線程2和3只能等待線程1釋放鎖A.因此他們只有在成功獲取到鎖A的狀況下,才能繼續獲取鎖B和鎖C.
順序獲取鎖是一個比較簡單且高效的預防死鎖的措施.然而,這種方式只能在你知道全部用到的鎖的前提下才有用,但實際狀況下並不老是這樣.
另外一個預防死鎖的方式是在線程等待獲取鎖的過程當中加入超時機制.即讓線程等待獲取鎖一段時間後超時.若是線程不能成功獲取它執行過程當中所須要的所有鎖則超時回滾,釋放全部它所持有的鎖,在一個給定的隨機時間後從新嘗試執行.給定的隨機時間讓其餘線程有機會去獲取它們所等待獲取的鎖,以便讓應用可以退出等待狀態繼續運行下去.
這裏是一個讓兩個線程嘗試以不一樣的順序獲取相同的鎖,且加入超時機制,讓線程可以回滾和從新嘗試執行的實例.
線程 1 持有鎖A
線程 2 持有鎖B
線程 1 嘗試獲取鎖B進入等待狀態
線程 2 嘗試獲取鎖A進入等待狀態
線程 1 等待獲取鎖B超時
線程 1 回滾和釋放鎖A
線程 1 等待隨機時間(300毫秒)後從新執行
線程 2 等待獲取鎖A超時
線程 2 回滾和釋放鎖B
線程 2 等待隨機時間(40毫秒)後從新執行
複製代碼
以上實例中,線程2領先線程260毫秒去嘗試從新執行,看起來是能夠成功獲取到全部鎖來知足執行的.線程A將會從新等待獲取鎖A.當線程2執行完畢後,線程1也可以獲取全部須要的鎖來完成執行.(除非線程2或其餘線程在線程1執行過程當中持有線程1所須要的鎖,則執行失敗).
有一點須要記住的是,線程獲取鎖超時並不意味着死鎖的發生.可能只是線程持有鎖後執行任務的時長過長已經超過了其餘線程等待獲取鎖的超時時長.
此外,若是有足夠的線程去競爭相同的資源,即便它們會超時和回滾,仍然有重複一次次持有其餘線程所須要鎖的風險.也許當只有兩個線程互相等待0~500毫秒並進行重試的狀況下這種風險不會放生,但若是是10到20個線程的狀況下就不必定了.在線程足夠多的狀況下,兩個線程等待重試的時間相同或是接近的概率就很高了.
更大的問題在於持有鎖超時的方式在Java同步代碼塊中不可能實現.咱們不能爲Javasynchronized
添加超時機制.你可能須要自定義鎖或是使用java5java.util.concurrency
包中的併發數據結構來實現.
死鎖檢測是一個在順序獲取鎖和持有鎖超時兩個措施都沒法使用的狀況下才使用的重量級措施.
每一次線程請求獲取和成功持有鎖的過程都會被記錄在能夠存儲線程和鎖關係的數據結構中(如Map).
當一個線程請求獲取鎖被拒絕後,線程能夠遍歷數據結構來檢查是否發生了死鎖.舉個例子,線程A請求獲取鎖7,而鎖7被線程B持有,線程A可以進行檢查線程B是否須要獲取線程A所持有的鎖,若是須要則死鎖發生(線程A持有鎖1, 嘗試獲取鎖7.線程B持有鎖7嘗試獲取鎖1).
固然死鎖的發生可以在超過兩個線程的狀況下.線程A等待線程B,線程B等待線程C,線程C等待線程D.爲了讓線程A對死鎖進行檢測,咱們須要知道線程B所請求的全部鎖;由於線程B請求的鎖被線程C持有,線程C請求的鎖被線程D持有,因此咱們須要知道連同線程C和D在內所請求的鎖.直到線程A發現它持有線程B所須要的全部鎖中的一個或多個爲止,則認定死鎖發生.
下圖展現了4個線程和它們所請求和持有鎖之間的關係.這樣一個數據結構可以用來檢測死鎖的發生.
當檢測到死鎖發生時,線程須要作什麼?
可讓線程回滾且釋放全部已持有的鎖,在等待一段隨機時長後從新嘗試運行.這種方式相似於持有鎖超時措施,區別在於僅在檢測到死鎖真實發生時,線程纔會觸發回滾.然而在競爭相同鎖的線程過多的狀況下,仍然會在屢次回滾和等待後從新進入死鎖狀態.
一個更加恰當的方式是爲線程分配優先級,只讓一個或少數幾個線程進行回滾.在這回滾的片刻時間裏,死鎖得以解決,讓其餘線程能夠獲取它們所須要的鎖繼續執行.若是給線程分配的優秀級是固定的,一樣的線程可能會一直佔據高優先級.固然咱們能夠在檢測到死鎖時隨機分配優先級來解決這個問題.
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial