死鎖是指兩個或兩個以上的執行緒在執行過程中,因爭奪資源而造成的一種互相等待的現象。若無外力作用,它們都將無法推進下去。
產生死鎖的四個必要條件得爛熟於心:
相應的,如果想在程式執行之前預防發生死鎖(也成為 「死鎖預防」),必須設法破壞產生死鎖的四個必要條件之一
光看羅列出來的幾點文字肯定還是不能完全理解,下面會結合範例來給大夥解釋。
這絕對是面試中 Java 手寫題的 TOP2!!!除了人盡皆知的手寫單例模式,手寫死鎖可能有些小夥伴會遺漏掉。
邏輯其實非常簡單,我們申請兩個資源,開兩個執行緒,每個執行緒持有其中的一個資源,並且互相請求對方的資源,就構成了死鎖。
下面來看個 MySQL 經典的死鎖案例:轉賬
A 賬戶給 B 賬戶轉賬 50 元的同時,B 賬戶也給 A 賬戶轉賬 30 元
正常情況下,如果只有一個操作,A 給 B 轉賬 50 元,可以在一個事務內完成,先獲取 A 使用者的餘額和 B 使用者的餘額,因為之後需要修改這兩條資料,所以需要通過寫鎖(for UPDATE)鎖住他們,防止其他事務更改導致我們的更改丟失而引起髒資料
但如果 A 給 B 轉賬和 B 給 A 轉賬同時發生,那就是兩個事務,可能發生死鎖:
1)A 使用者給 B 使用者轉賬 50 元,需在程式中開啟事務 1 來執行 SQL,獲取 A 的餘額同時鎖住 A 這條資料。
2)B 使用者給 A 使用者轉賬 30 元,需在程式中開啟事務 2 來執行 SQL,並獲取 B 的餘額同時鎖住 B 這條資料。
3)在事務 1 中執行剩下的 SQL,此時事務 1 是獲取不到 B 的鎖的,也即 select for update 就會被阻塞住;
4)同理,事務 2 繼續執行剩下的 SQL,請求 A 的鎖,也是獲取不到的
事務 1 和事務 2 存在相互等待獲取鎖的過程,導致兩個事務都掛起阻塞,最終丟擲獲取鎖超時的異常。
要想解決上述死鎖問題,我們可以從死鎖的四個必要條件入手。
指導思想其實很明確:就是保證 A 向 B 轉賬和 B 向 A 轉賬這兩個事務同一時刻只能有一個事務能成功獲取到鎖
由於互斥和不剝奪是鎖本質的功能體現,無法修改,所以咱們從另外兩個條件嘗試去解決。
1)破壞 「請求和保持」 條件:A 和 B 之間的操作用同一個鎖鎖住(比如用 Redis 分散式鎖做,A 和 B 之間的鎖的 key 表示為 A:B
,可以讓 id 小的使用者排在前面,id 大的使用者排在後面,這樣來設計 key。如果存在分庫分表的情況,用 hashcode 來做比較也行),保證 A 向 B 轉賬和 B 向 A 轉賬這兩個事務同一時刻只能有一個事務能成功獲取鎖
2)破壞 「迴圈等待」 條件:先獲取更小的鎖,獲取到了小的鎖才能獲取大鎖(所謂小鎖還是大鎖,也可以簡單的根據使用者的 id 來進行區分,先請求使用者 id 較小的,再請求使用者 id 較大的)。比如 A.id < B.id,那麼 A 和 B 之間的操作,都是要先獲取 A 鎖,再獲取 B 鎖
具體程式碼可參考如下:
小夥伴們大家好呀,本文首發於公眾號@飛天小牛肉,阿里雲 & InfoQ 簽約作者,分享大廠面試原創高質量題解、原創技術幹活和成長經驗~)