在安全性與活躍性之間一般存在着某些制衡。java
鎖順序死鎖(Lock-Ordering Deadlock)
。資源死鎖(Resource Deadlock)
。在數據庫系統的設計中考慮了檢測死鎖以及從死鎖中恢復。當數據庫系統檢測到一組事務發生了死鎖(經過在表示等待關係的有向圖中搜索循環),將選擇一個犧牲者,釋放其所有資源,放棄這個事務。
JVM解決死鎖的問題遠沒有數據庫服務那麼強大,當一組java線程發生死鎖時,這些線程將永遠不能再使用了。根據線程完成工做的不一樣,可能形成應用程序徹底中止,或者某個特意子系統中止,或是性能下降。恢復應用程序的惟一方式就是停止並重啓它。
與其餘的併發危險同樣,一個類有可能發生死鎖,並非每次都會發生死鎖。在死鎖發生的時候,每每是最糟糕的時候--高負載狀況下。數據庫
這個是最多見的死鎖發生方式:線程1和線程2,線程1調用A(),得到了鎖A,想要繼續調用B();線程2調用了B(),得到了鎖B,想要繼續調用A()。兩個線程都在等待對方釋放資源。
這裏死鎖的緣由,是由於兩個線程試圖以不一樣的順序得到相同的鎖。若是按照相同的順序來請求鎖,那麼就不會出現循環的加鎖依賴性。也就不會發生死鎖了。安全
若是全部線程以固定的順序來得到鎖,那麼程序中就不會出現鎖順序死鎖的問題。
有時候,並不能清楚地知道是否在鎖順序上有足夠的控制權來避免死鎖的發生。併發
/** * 容易發生死鎖 */ public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException { synchronized(fromAccount){ synchronized(toAccount){ if(fromAccount.getBalance.compareTo(account) < 0){ throw new InsufficientFundsException(); } else { fromAccount.debit(amount); toAccount.credit(amount); } } } }
全部的線程看起來都是按照相同的順序來獲取鎖,事實上鎖的順序取決於傳遞給transferMoney的順序。若是一個線程從X向Y轉帳,另外一個線程從Y向X轉帳,那麼就有可能發送鎖順序死鎖。ide
private static final Object tieLock = new Object(); public void transferMoney(final Account fromAcct, final Account toAcct, final DollarAmount amount) throws InsufficientFundsException { class Helper{ public void transfer() throws throws InsufficientFundsException { if(fromAccount.getBalance.compareTo(account) < 0){ throw new InsufficientFundsException(); } else { fromAccount.debit(amount); toAccount.credit(amount); } } } int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if(fromHash < toHash){ synchronized(fromAccount){ synchronized(toAccount){ new Helper().transfer(); } } } else if(fromHash > toHash){ synchronized(toAccount){ synchronized(fromAcct){ new Helper().transfer(); } } }else{ synchronized(tieLock){ synchronized(toAccount){ synchronized(fromAcct){ new Helper().transfer(); } } } } }
在極少數狀況下,兩個對象可能擁有相同的散列值,此時必須經過某種任務的方法來決定鎖的順序,不然可能又會陷入死鎖。爲了不這種狀況,引入了「加時賽(Tie-Breaking)」鎖。在獲取兩個鎖以前,首先得到這個「加時賽」鎖,從而保證只有一個線程以未知的順序得到這兩個鎖,從而消除了死鎖發生的可能性。
若是Account中包含一個惟一的,不可變的且具有可比性的鍵值,好比id,帳號,那麼只須要根據鍵值對對象進行排序,不須要使用「加時賽」鎖。性能
若是在持有鎖時調用某個外部方法,那麼將出現活躍性問題。在外部方法中可能得到其餘鎖(這可能會產生死鎖),或阻塞時間過長,致使其餘線程沒法即便得到當前被持有的鎖。
方法調用至關於一種抽象屏障,由於你無需瞭解被調用方法中所執行的操做。但也正是因爲不知道在被調用方法中執行的操做,所以在持有鎖的時候對調用某個外部方法難以進行分析,從而可能出現死鎖。
若是在調用某個方法時不須要持有鎖,那麼這種調用被稱爲開放調用(Open Call)
。依賴於開放調用的類一般能表現出更好的行爲,而且與那些在調用方法時須要持有鎖的類相比,也更容易編寫。經過儘量的使用開放調用,將更容易找出那些須要獲取多個鎖的代碼路徑,所以也更容易確保採用一致的順序來獲取鎖。操作系統
在程序中應儘可能使用開放調用。與那些在持有鎖時調用外部方法的程序相比,更容易對依賴於開放調用的程序進行死鎖分析。
正如當多個線程相互持有彼此正在等待的鎖而又不釋放本身已持有的鎖時會發生死鎖,當它們在相同的資源集合上等待時,也會發生死鎖。
假設有兩個資源池,例如兩個不一樣數據庫的鏈接池。資源池一般採用信號量來實現當資源池爲空時的阻塞行爲。若是一個任務須要鏈接兩個數據庫,而且在請求這兩個資源時不會始終遵照相同的順序,那麼也可能出現死鎖(資源池越大,死鎖機率越小)。
另外一種基於資源的死鎖形式就是線程飢餓死鎖
:一個任務提交另外一個任務,並等待被提交任務在單線程的Executor中執行完成。這種狀況下,第一個任務將永遠的等待下去,並使得另外一個任務以及在這個Executor中執行的全部其餘任務都中止執行。若是某些任務須要等待其餘任務的結果,那麼這些任務每每是產生線程飢餓死鎖的主要來源。有界線程池/資源池與相互依賴的任務不能一塊兒使用。線程
若是一個程序每次最多隻能獲取一個鎖,那麼就不會產生鎖順序死鎖。固然,這種狀況一般並不現實。若是必須得到不少鎖,那麼在設計時必須考慮鎖的順序:儘可能減小潛在的加鎖交互數量,將獲取鎖時須要遵循的協議寫入文檔並始終遵照。
在使用細粒度鎖的程序中,能夠經過使用一種兩階段策略(Two-Part Strategy)
來檢查代碼中的死鎖:設計
有一項技術能夠檢測死鎖和從死鎖中恢復過來,即顯式使用Lock類中的定時tryLock功能
來代替內置鎖機制。當使用內置鎖的時,只要沒得到到鎖,就會永遠的等待下去,而顯式鎖則能夠指定一個超時時限,在等待超過該時間後,tryLock會返回一個失敗信息。若是超時時限比獲取鎖的時間長不少,那麼能夠在發生某個意外狀況後從新得到控制權。
當定時鎖失敗時,你並不須要知道失敗的緣由,或許是由於發生了死鎖,或許是某個線程在持有鎖時錯誤進入了無限循環,還有多是某個操做的執行時間遠遠超過了你的預期。然而至少你能記錄所發生的失敗,以及關於此次操做的其餘有用信息,並經過一種更平緩的方式從新啓動計算,而不是關閉整個進程。
即便在整個系統中沒有始終使用定時鎖,使用定時鎖來獲取多個鎖也能有效的應對死鎖問題。若是在獲取鎖時超時,那麼就釋放這個鎖,而後後退一段時間後再次嘗試,從而消除了發生死鎖的條件,使程序恢復過來。(只有在同時獲取兩個鎖時纔有效,若是若是在嵌套的方法調用中請求多個鎖,那麼即便你知道已經持有了外層的鎖,也沒法釋放它)。code
儘管死鎖是最多見的活躍性危險,但在併發程序中還存在一些活躍性危險:飢餓
、丟失信號
、活鎖
等。
當線程因爲沒法訪問它所需的資源而不能執行時,就發生了飢餓(Starvation)
。引起飢餓的最多見資源就是CPU時鐘週期,若是在java應用程序中對線程的優先級使用不當,或者在持有鎖時執行一些沒法結束的結構(例如無限循環、無限等待資源等),那麼也有可能致使飢餓,由於其餘須要這個鎖的線程將沒法獲得它。
操做系統的線程調度器會盡力提供公平的,活躍性良好的調度,甚至遠超出java語言規範的需求範圍。在大多數java應用程序中,全部線程都具備相同的優先級Thread.NORMAL_PRIORITY。線程優先級並非直觀的機制,提升了優先級之後可能起不到任何做用,也可能使得某個線程的調度優先於其餘線程,從而致使飢餓。
咱們要儘可能避免使用線程優先級,由於這會增長平臺依賴性,並可能致使活躍性問題。在絕大多數併發應用中,使用默認的線程優先級便可。
不良的鎖管理可能致使糟糕的響應,例如某個線程長時間佔有一個鎖(或許正在對一個超大容器進行迭代,並對每一個元素進行計算密集的處理),從而使其餘想要訪問這個容器的線程就必須等待很長時間。
活鎖(Livelock)
是另外一種形式的活躍性問題,該問題儘管不會阻塞線程,可是也不能繼續執行,由於線程將不斷重複執行相同的操做,並且老是會失敗。活鎖一般發生在處理事務消息的應用程序中:若是不能成功處理某個消息,那麼消息處理機制將回滾整個事務,並將它從新放入這個隊列的開頭。再次執行到,又都會發出錯誤並回滾,所以處理器將被反覆調用,雖然線程並無阻塞,可是也沒法執行下去。這種活鎖一般是由過分的錯誤恢復代碼形成的:錯誤的將不可修復的錯誤做爲可修復的錯誤。
要解決活鎖問題,須要在重試機制中引入隨機性。例如經過等待隨機長度和回退能夠有效地避免活鎖發生。
活躍性問題是一個很是嚴重的問題,由於當出現活躍性問題時,除了停止應用程序以外沒有其餘任何機制能夠幫助從這種故障中恢復。最多見的活躍性問題就是鎖順序死鎖。在設計的時候應該要避免嘗試鎖順序死鎖,確保線程在獲取多個鎖的時候保持一致的順序。若是狀況容許,儘量多的使用開放調用。