死鎖是多線程編程或者說是併發編程中的一個經典問題,也是咱們在實際工做中極可能會碰到的問題。相信大部分讀者對「死鎖」這個詞都是略有耳聞的,但從我對後端開發崗位的面試狀況來看不少同窗每每對死鎖都尚未系統的瞭解。雖然「死鎖」聽起來很高深,可是實際上已經被研究得比較透徹,大部分的解決方法都很是成熟和清晰,因此你們徹底不用擔憂這篇文章的難度。java
雖然本文是一篇介紹死鎖及其解決方式的文章,可是對於多線程程序中的非死鎖問題咱們也應該有所瞭解,這樣才能寫出正確且高效的多線程程序。多線程程序中的非死鎖問題主要分爲兩類:程序員
違反原子性問題面試
違反執行順序問題sql
接下來就讓咱們開始消滅死鎖吧!數據庫
死鎖,顧名思義就是致使線程卡死的鎖衝突,例以下面的這種狀況:編程
能夠看出,上面的兩個線程已經互相卡死了,線程t1在等待線程t2釋放鎖B,而線程t2在等待線程t1釋放鎖A。兩個線程各執己見也就沒有一個線程能夠繼續往下執行了。這種狀況下就發生了死鎖。後端
上面的狀況只是死鎖的一個例子,咱們能夠用更精確的方式描述死鎖出現的條件:數組
上面這四個都是死鎖出現的必要條件,若是其中任何一個條件不知足都不會出現死鎖。雖然這四個條件的定義看起來很是的理論和官方,可是在實際的編程實踐中,咱們正是在死鎖的這四個必要條件基礎上構建出解決方案的。因此這裏不妨思考一下這四個條件各自的含義,想想若是去掉其中的一個條件死鎖是否還能發生,或者爲何不能發生。安全
瞭解了死鎖的概念和四個必要條件以後,咱們下面就正式開始解決死鎖問題了。對於死鎖問題,咱們最但願可以達到的固然是徹底不發生死鎖問題,也就是在死鎖發生以前就阻止它。數據結構
那麼想要阻止死鎖的發生,咱們天然是要讓死鎖沒法成立,最直接的方法固然是破壞掉死鎖出現的必要條件。只要有任何一個必要條件沒法成立,那麼死鎖也就沒辦法發生了。
實踐中最有效也是最經常使用的一種死鎖阻止技術就是鎖排序,經過對加鎖的操做進行排序咱們就可以破壞環路等待條件。例如當咱們須要獲取數組中某一個位置對應的鎖來修改這個位置上保存的值時,若是須要同時獲取多個位置對應的鎖,那麼咱們就能夠按位置在數組中的排列前後順序統一從前日後加鎖。
試想一下若是程序中全部須要加鎖的代碼都按照一個統一的固定順序加鎖,那麼咱們就能夠想象鎖被放在了一條不斷向前延伸的直線上,而由於加鎖的順序必定是沿着這條線向下走的,因此每條線程都只能向前加鎖,而不能再回頭獲取已經在後面的鎖了。這樣一來,線程只會向前單向等待鎖釋放,天然也就沒法造成一個環路了。
其實大部分死鎖解決方法不止能夠用於多線程編程領域,還能夠擴展到更多的併發場景下。好比在數據庫操做中,若是咱們要對某幾行數據執行更新操做,那麼就會獲取這幾行數據所對應的鎖,咱們一樣能夠經過對數據庫更新語句進行排序來阻止在數據庫層面發生的死鎖。
可是這種方案也存在它的缺點,好比在大型系統當中,不一樣模塊直接解耦和隔離得很是完全,不一樣模塊的研發同窗之間都不清楚具體的實現細節,在這樣的狀況下就很難作到整個系統層面的全局鎖排序了。在這種狀況下,咱們能夠對方案進行擴充,例如Linux在內存映射代碼就使用了一種鎖分組排序的方式來解決這個問題。鎖分組排序首先按模塊將鎖分爲了避免同的組,每一個組之間定義了嚴格的加鎖順序,而後再在組內對具體的鎖按規則進行排序,這樣就保證了全局的加鎖順序一致。在Linux的對應的源碼頂部,咱們能夠看到有很是詳盡的註釋定義了明確的鎖排序規則。
這種解決方案若是規模過大的話即便能夠實現也會很是的脆弱,只要有一個加鎖操做沒有遵照鎖排序規則就有可能會引起死鎖。不過在像微服務之類解耦比較充分的場景下,只要架構拆分合理,任務模塊儘量小且不會將加鎖範圍擴大到模塊以外,那麼鎖排序將是一種很是實用和便捷的死鎖阻止技術。
想要破壞持有並等待條件,咱們能夠一次性原子性地獲取全部須要的鎖,好比經過一個專門的全局鎖做爲加鎖令牌控制加鎖操做,只有獲取了這個鎖才能對其餘鎖執行加鎖操做。這樣對於一個線程來講就至關於一次性獲取到了全部須要的鎖,且除非等待加鎖令牌不然在獲取其餘鎖的過程當中不會發生鎖等待。
這樣的解決方案雖然簡單粗暴,但這種簡單粗暴也帶來了一些問題:
若是一個線程已經獲取到了一些鎖,那麼在這個線程釋放鎖以前這些鎖是不會被強制搶佔的。可是爲了防止死鎖的發生,咱們能夠選擇讓線程在獲取後續的鎖失敗時主動放棄本身已經持有的鎖並在以後重試整個任務,這樣其餘等待這些鎖的線程就能夠繼續執行了。
一樣的,這個方案也會有本身的缺陷:
雖然這種方式能夠避免死鎖,可是若是幾個互相存在競爭的線程不斷地放棄、重試、放棄,那麼就會致使活鎖問題(livelock)。在這種狀況下,雖然線程沒有由於鎖衝突被卡死,可是仍然會被阻塞至關長的時間甚至一直處於重試當中。
雖然每個方案都有本身的缺陷,可是在適合它們的場景下,它們都能發揮出巨大的做用。
在以前的文章中,咱們已經瞭解了一種與鎖徹底不一樣的同步方式CAS。經過CAS提供的原子性支持,咱們能夠實現各類無鎖數據結構,不只避免了互斥鎖所帶來的開銷和複雜性,也由此避開了咱們一直在討論的死鎖問題。
AtomicInteger
類中就大量使用了CAS操做來實現併發安全,例如incrementAndGet()
方法就是用Unsafe
類中基於CAS的原子累加方法getAndAddInt
來實現的。下面是Unsafe
類的getAndAddInt
方法實現:
/** * 增長指定字段值並返回原值 * * @param obj 目標對象 * @param valueOffset 目標字段的內存偏移量 * @param increment 增長值 * @return 字段原值 */ public final int getAndAddInt(Object obj, long valueOffset, int increment) { // 保存字段原值的變量 int oldValue; do { // 獲取字段原值 oldValue = this.getIntVolatile(obj, valueOffset); // obj和valueOffset惟一指定了目標字段所對應的內存區域 // while條件中不斷調用CAS方法來對目標字段值進行增長,並保證字段的值沒有被其餘線程修改 // 若是在修改過程當中其餘線程修改了這個字段的值,那麼CAS操做失敗,循環語句會重試操做 } while(!this.compareAndSwapInt(obj, valueOffset, oldValue, oldValue + increment)); // 返回字段的原值 return oldValue; }
上面代碼中的compareAndSwapInt
方法就是咱們說的CAS操做(Compare And Swap),咱們能夠看到,CAS在每次執行時不必定會成功。若是執行CAS操做時目標字段的值已經被別的線程修改了,那麼此次CAS操做就會失敗,循環語句將會在CAS操做失敗的狀況下不斷重試一樣的操做。這種不斷重試的方式就被稱爲自旋,在jvm當中對互斥鎖的等待也會經過少許的自旋操做來進行優化。
不過若是一個變量同時被多個線程以CAS方式修改,那麼就有可能致使出現活鎖,多個線程將會一直不斷重試CAS操做。因此CAS操做的成本和數據競爭的激烈程度密切相關,在一些競爭很是激烈的狀況下,CAS操做的成本甚至會超過互斥鎖。
除了累加整型值這樣的簡單場景以外,還有更多更復雜的無鎖(lock-free)數據結構,例如java.util.concurrent
包中的ConcurrentLinkedDeque
雙端隊列類就是一個無鎖的併發安全鏈表實現,有興趣的讀者能夠了解一下。
這種方法一樣能夠用在數據庫操做上,當咱們執行update語句時能夠在where子句中添加上一些字段的舊值做爲條件,好比update t_xxxx set value = <newValue>, version = version + 1 where id = xxx and version = 10
,這樣咱們就能夠經過update語句返回的影響行數是否是0來判斷更新操做有沒有成功了,這是否是和CAS很類似?
有時,咱們並不須要徹底阻止死鎖的發生,而是能夠經過其餘的手段來控制死鎖的影響。就像若是新的治療手段可使癌症病人繼續活七八十年,那麼癌症也就沒有那麼可怕了。
還有一種解決死鎖的方法就是讓死鎖發生,以後再解決它,就像電腦死機之後直接重啓同樣。使用這種方法咱們能夠這麼作:若是多個線程出現了死鎖的狀況,那麼咱們就殺死足夠多的線程使系統恢復到可運行狀態。在咱們經常使用的關係型數據庫中使用的就是這種方法,數據庫會週期性地使用探測器建立資源圖,而後檢查其中是否存在循環。若是探測到了循環(死鎖),那麼數據庫就會根據估算的執行成本高低殺死能夠解決死鎖問題的儘量成本最小的線程。
數據庫在被外部應用調用的過程當中是沒辦法獲知外部應用的邏輯細節的,因此天然也就沒辦法用以前說的種種方法來解決死鎖問題,只能經過過後檢測並恢復來對死鎖問題作最低限度的保障。可是咱們能夠在咱們的應用程序中應用更多的解決方案,從更上層解決死鎖問題。
在這篇文章中,咱們從死鎖的概念出發,首先介紹了死鎖是什麼和死鎖發生的四個必要條件。而後經過破壞任意一個必要條件產生了四種不一樣的阻止死鎖的解決方案,最後介紹了另一種死鎖解決方法——在死鎖發生後再探測並恢復系統運行。相信你們能夠在不一樣的場景中都能找到適合該場景的解決方案,可是鎖本質上是容易引入問題的,因此若是不是確有必要,最好不要貿然用鎖來進行處理。
分享免費學習資料
針對於Java程序員,我這邊準備免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)
爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!
資料領取方式:加入Java技術交流羣963944895
,點擊加入羣聊,私信管理員便可免費領取