避免活躍性危險(第十章)
避免活躍性危險
在安全性與活躍性之間通常存在著某種製衡,我們使用加鎖機製來確保線程安全,但如果過度地使用加鎖,則可能導致“鎖順序死鎖”。同樣,我們使用線程池和信號量來限製對資源的使用,但這些被限製的行為可能會導致資源死鎖。
1. 死鎖
鎖順序死鎖:兩個線程試圖以不同的順序來獲得相同的鎖,如果按照相同的順序來請求鎖,那麼就不會出現循環的加鎖依賴性,因此也就不會產生死鎖。
在製定鎖的順序時,可以使用System.identityHashCode方法,該方法返回有Object.hashCode返回的值,通過比較大小等方法定義鎖的順序。在某些情況下,兩個對象可能擁有相同的散列值,此時必須通過某種方法來決定鎖的順序,而這可能會重新引入死鎖,為了避免這種情況,可以使用“加時賽”鎖,在獲得兩個對象的鎖之前,首先獲得這個加時賽鎖,從而保證每次隻有一個線程以未知的順序獲得這兩個鎖。在協作對象之間發生的死鎖
如果在持有鎖的情況下調用某個外部方法,那麼就需要警惕在協作對象之間發生死鎖。
如果在持有鎖時調用某個外部方法,那麼將出現活躍性問題,在這個外部方法中可能會獲得其他鎖(這可能會產生死鎖),或者阻塞時間過長,導致其他線程無法及時獲得當前被持有的鎖。開放調用
如果在調用某個方法時不需要持有鎖,那麼這種調用被稱為開放調用。
在程序中應盡量使用開放調用。與那些在持有鎖時調用外部方法的程序相比,更易於對依賴於開放調用的程序進行死鎖分析。資源死鎖
線程饑餓死鎖。如果某些任務需要等待其他任務的結果,那麼這些任務往往是產生線程饑餓死鎖的主要來源。
有界線程池/資源池與相互依賴的任務不能一起使用
2. 死鎖的避免與診斷
- 如果一個程序每次至多隻能獲得一個鎖,那麼就不會產生鎖順序死鎖。
- 如果必須獲取多個鎖,那麼在設計時必須考慮鎖的順序:盡量減少潛在的加鎖交互數量,將獲取鎖時需要遵循的協議寫入正式文檔並始終遵循這些協議。
- 在使用細粒度鎖的程序中,可以通過使用一種兩階段策略來檢查代碼中的死鎖:首先,找出在什麼地方將獲取多個鎖,然後對所有這些實例進行全局分析,從而確保它們在整個程序中獲取鎖的順序都保持一致。
- 使用顯示鎖Lock類中的定時tryLock功能,在等待超過指定時間後tryLock會放回一個失敗信息。
3. 其他活躍性危險
-
饑餓:當線程由於無法訪問它所需要的資源而不能繼續執行時,就發生了“饑餓”。
引發饑餓的最常見資源就是CPU時鍾周期,如果在Java應用程序中對線程的優先級使用不當,或者在持有鎖時執行一些無法結束的結構(例如無限循環,或者無限製地等待某個資源),那麼也可能導致饑餓,因為其他需要這個鎖的線程將無法得到它。線程優先級並不是一種直觀的機製,而通過修改線程優先級所帶來的效果通常也不明顯。當提高某個線程的優先級時,可能不會起到任何作用,或者也可能使得某個線程的調度優先級高於其他線程,從而導致饑餓。
通常,我們盡量不要改變線程的優先級。隻要改變了線程的優先級,程序的行為就將與平台相關,並且會導致發生饑餓問題的風險。 Thread.yield以及Thread.sleep的語義都是UB,JVM既可以將他們實現為空操作,也可以將它們視為線程調度的參考。
活鎖(Livelock)
活鎖是另一種形式的活躍性問題,該問題不會導致線程阻塞,但也不能繼續執行。因為線程將不斷重複執行相同的操作,而且總會失敗。
活鎖通常發生在處理事務消息的應用程序中:如果不能成功處理某個消息,那麼消息處理機製將回滾整個事務,並將它重新放到隊列的開頭。當多個相互協作的線程都對彼此進行響應從而修改各自的狀態,並使得任何一個線程無法繼續執行時,就發生了活鎖,在並發應用程序中,通過等待隨機長度的時間和回退可以有效避免活鎖的發生。
最後更新:2017-11-04 18:33:44