什么是死鎖:
是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖。
舉個例子:
A 和 B 去按摩洗腳,都想在洗腳的時候,同時順便做個頭部按摩,13 技師擅長足底按摩,14 擅長頭部按摩。
這個時候 A 先搶到 14,B 先搶到 13,兩個人都想同時洗腳和頭部按摩,於是就互不相讓,揚言我死也不讓你,這樣的話,A 搶到 14,想要 13,B 搶到 13,想要 14,在這個想同時洗腳和頭部按摩的事情上 A 和 B 就產生了死鎖。
怎么解決這個問題呢?
第一種,假如這個時候,來了個 15,剛好也是擅長頭部按摩的,A 又沒有兩個腦袋,自然就歸了 B,於是 B 就美滋滋的洗腳和做頭部按摩,剩下 A 在旁邊氣鼓鼓的,這個時候死鎖這種情況就被打破了,不存在了。
第二種,C 出場了,用武力強迫 A 和 B,必須先做洗腳,再頭部按摩,這種情況下,A 和 B 誰先搶到 13,誰就可以進行下去,另外一個沒搶到的,就等着,這種情況下,也不會產生死鎖。
總結一下:
死鎖是必然發生在多操作者(M>=2 個)情況下,爭奪多個資源(N>=2 個,且 N<=M)才會發生這種情況。很明顯,單線程自然不會有死鎖,只有 B 一個去,不要 2 個,打十個都沒問題;單資源呢?只有 13,A 和 B 也只會產生激烈競爭,打得不可開交,誰搶到就是誰的,但不會產生死鎖。同時,死鎖還有一個重要的要求,爭奪資源的順序不對,如果爭奪資源的順序是一樣的,也不會產生死鎖。
死鎖的發生下四個必要條件:
理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。
如何有效預防死鎖的發生:
只要打破四個必要條件之一就可以了
打破互斥條件:改造獨占性資源為虛擬資源,大部分資源已無法改造。
打破不可搶占條件:當一進程占有一獨占性資源后又申請一獨占性資源而無法滿足,則退出原占有的資源。
打破占有且申請條件:采用資源預先分配策略,即進程運行前申請全部資源,滿足則運行,不然就等待,這樣就不會占有且申請。
打破循環等待條件:實現資源有序分配策略,對所有設備實現分類編號,所有進程只能采用按序號遞增的形式申請資源。
避免死鎖常見的算法:
有有序資源分配法、銀行家算法。
現象、危害和解決
在我們 IT 世界有沒有存在死鎖的情況,有:數據庫里多事務而且要同時操作多個表的情況下。所以數據庫設計的時候就考慮到了檢測死鎖和從死鎖中恢復的機制。比如 oracle 提供了檢測和處理死鎖的語句,而 mysql 也提供了“循環依賴檢測的機制”
現象
簡單順序死鎖示例:
/** * @ClassName NormalDeadLock * @Description TODO 簡單順序死鎖測試 * @Date 2020/5/10 16:07 **/ public class NormalDeadLock { private static Object valueFirst = new Object();//第一個鎖 private static Object valueSecond = new Object();//第二個鎖 //先拿第一個鎖,再拿第二個鎖 private static void fisrtToSecond() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (valueFirst) { System.out.println(threadName + " get 1st"); Thread.sleep(100); synchronized (valueSecond) { System.out.println(threadName + " get 2nd"); } } } //先拿第二個鎖,再拿第一個鎖 private static void SecondToFisrt() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (valueSecond) { System.out.println(threadName + " get 2nd"); Thread.sleep(100); synchronized (valueFirst) { System.out.println(threadName + " get 1st"); } } } private static class TestThread extends Thread { private String name; public TestThread(String name) { this.name = name; } public void run() { Thread.currentThread().setName(name); try { SecondToFisrt(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread.currentThread().setName("TestDeadLock"); TestThread testThread = new TestThread("SubTestThread"); testThread.start(); try { fisrtToSecond(); } catch (InterruptedException e) { e.printStackTrace(); } } }
簡單順序死鎖解決方案:在鎖的時,兩個方法都 先拿第一個鎖,再拿第二個鎖
動態順序死鎖及解決方案代碼示例
- 用戶賬戶的實體類
- 銀行轉賬動作接口
- 不安全的轉賬動作的實現(動態順序死鎖)
- 不會產生死鎖的安全轉賬一(解決動態順序死鎖方案一)
- 不會產生死鎖的安全轉賬二(解決動態順序死鎖方案二)
- main函數測試類
用戶賬戶的實體類代碼示例
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 類說明:用戶賬戶的實體類 */ public class UserAccount { private final String name;//賬戶名稱 private int money;//賬戶余額 private final Lock lock = new ReentrantLock(); public Lock getLock() { return lock; } public UserAccount(String name, int amount) { this.name = name; this.money = amount; } public String getName() { return name; } public int getAmount() { return money; } @Override public String toString() { return "UserAccount{" + "name='" + name + '\'' + ", money=" + money + '}'; } //轉入資金 public void addMoney(int amount) { money = money + amount; } //轉出資金 public void flyMoney(int amount) { money = money - amount; } }
銀行轉賬動作接口代碼示例
/** * 類說明:銀行轉賬動作接口 */ public interface ITransfer { void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException; }
不安全的轉賬動作的實現(動態順序死鎖)代碼示例
/** * 類說明:不安全的轉賬動作的實現(動態順序死鎖) */ public class TrasnferAccount implements ITransfer { /** * @param from--轉出對象 * @param to--轉入對象 * @param amount--賬戶余額 * @throws InterruptedException */ @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { synchronized (from) { System.out.println(Thread.currentThread().getName() + " get" + from.getName()); Thread.sleep(100); synchronized (to) { System.out.println(Thread.currentThread().getName() + " get" + to.getName()); from.flyMoney(amount); to.addMoney(amount); } } } }
不會產生死鎖的安全轉賬一(解決動態順序死鎖方案一)代碼示例
/** * 類說明:不會產生死鎖的安全轉賬一(解決動態順序死鎖方案一) * 原理: * 通過比較hash值決定線程的執行順序 * 具體實現思路: * 1. 獲取各個對象的hash值 * 2.1 若hash值小則先鎖對象一,再鎖對象二 * 2.2 若hash值大則先鎖對象二,再鎖對象一 * 2.3 若hash值相同,則在第三把鎖的基礎上,再同時加鎖(Ps:出現此情況的幾率特別小,所以在保證線程的安全基礎上,不必太在意這一點性能) */ public class SafeOperate implements ITransfer { private static Object tieLock = new Object();//第三把鎖 /** * @param from--轉出對象 * @param to--轉入對象 * @param amount--賬戶余額 * @throws InterruptedException */ @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { // 獲取對象的hash值 int fromHash = System.identityHashCode(from); int toHash = System.identityHashCode(to); // 通過比較hash值決定線程的執行順序 if (fromHash < toHash) { synchronized (from) { System.out.println(Thread.currentThread().getName() + " get " + from.getName()); Thread.sleep(100); synchronized (to) { System.out.println(Thread.currentThread().getName() + " get " + to.getName()); from.flyMoney(amount); to.addMoney(amount); System.out.println(from); System.out.println(to); } } } else if (toHash < fromHash) { synchronized (to) { System.out.println(Thread.currentThread().getName() + " get" + to.getName()); Thread.sleep(100); synchronized (from) { System.out.println(Thread.currentThread().getName() + " get" + from.getName()); from.flyMoney(amount); to.addMoney(amount); System.out.println(from); System.out.println(to); } } } else { // 若hash值相同,則在第三把鎖的基礎上,再同時加鎖(Ps:出現此情況的幾率特別小,所以在保證線程的安全基礎上,不必太在意這一點性能) synchronized (tieLock) { synchronized (from) { synchronized (to) { from.flyMoney(amount); to.addMoney(amount); } } } } } }
不會產生死鎖的安全轉賬二(解決動態順序死鎖方案二)代碼示例
import java.util.Random; /** * 類說明:不會產生死鎖的安全轉賬二(解決動態順序死鎖方案二) * 原理: * 獲取所有的鎖后再繼續執行 * 具體實現思路: * 1.若未獲取第一把鎖則while無限循環 * 2.獲取第一把鎖后,若無法獲取第二把鎖則釋放第一把鎖繼續循環 * 3.獲取第一把鎖,同時獲取第二把鎖時執行任務 * 4.結束 */ public class SafeOperateToo implements ITransfer { /** * @param from--轉出對象 * @param to--轉入對象 * @param amount--賬戶余額 * @throws InterruptedException */ @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { // 隨機數:避免活鎖 Random r = new Random(); while (true) { if (from.getLock().tryLock()) { System.out.println(Thread.currentThread().getName() + " get" + from.getName()); try { if (to.getLock().tryLock()) { try { System.out.println(Thread.currentThread().getName() + " get" + to.getName()); from.flyMoney(amount); to.addMoney(amount); System.out.println(from); System.out.println(to); // 執行完任務后退出 break; } finally { to.getLock().unlock(); } } } finally { from.getLock().unlock(); } } //錯開時間,避免活鎖 Thread.sleep(r.nextInt(5)); } } }
main函數測試類代碼示例
/** * 類說明:模擬支付公司轉賬的動作 * 測試類 */ public class PayCompany { /*執行轉賬動作的線程*/ private static class TransferThread extends Thread { private String name; private UserAccount from; private UserAccount to; private int amount; private ITransfer transfer; public TransferThread(String name, UserAccount from, UserAccount to, int amount, ITransfer transfer) { this.name = name; this.from = from; this.to = to; this.amount = amount; this.transfer = transfer; } public void run() { Thread.currentThread().setName(name); try { transfer.transfer(from, to, amount); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { PayCompany payCompany = new PayCompany(); // 對象一 UserAccount zhangsan = new UserAccount("zhangsan", 20000); // 對象二 UserAccount lisi = new UserAccount("lisi", 20000); // 線程不安全:TrasnferAccount //ITransfer transfer = new TrasnferAccount(); // 線程安全:SafeOperate //ITransfer transfer = new SafeOperate(); // 線程安全:SafeOperateToo ITransfer transfer = new SafeOperateToo(); // 張三轉李四 TransferThread zhangsanToLisi = new TransferThread("zhangsanToLisi", zhangsan, lisi, 2000, transfer); // 李四轉張三 TransferThread lisiToZhangsan = new TransferThread("lisiToZhangsan", lisi, zhangsan, 4000, transfer); // 線程開始執行 zhangsanToLisi.start(); lisiToZhangsan.start(); } }
危害
1、線程不工作了,但是整個程序還是活着的
2、沒有任何的異常信息可以供我們檢查。
3、一旦程序發生了發生了死鎖,是沒有任何的辦法恢復的,只能重啟程序,對生產平台的程序來說,這是個很嚴重的問題。
實際工作中的死鎖
時間不定,不是每次必現;一旦出現沒有任何異常信息,只知道這個應用的所有業務越來越慢,最后停止服務。。。。
若發生死鎖,如何定位死鎖?
1.打開cmd
2.cmd切換路徑到jdk的bin目錄下
3.檢查是否有死鎖信息,輸入:jps -
回車:若是有死鎖則會打印出死鎖的具體信息,若是沒有則會出現以下`數據
4.定位死鎖的具體信息,輸入:jstack 序號(jstack+空格+死鎖線程的Id)
回車:即會打印死鎖線程的具體信息
定位死鎖示例:
cmd 定位死鎖:
若有IDEA,左側的小照相機圖標也可定位死鎖:
死鎖解決方案
關鍵是保證拿鎖的順序一致
兩種解決方式(參考以上代碼↑↑↑)
1、內部通過順序比較,確定拿鎖的順序;
2、采用嘗試拿鎖的機制。
其他安全問題
活鎖(參考以上代碼)
兩個線程在嘗試拿鎖的機制中,發生多個線程之間互相謙讓,不斷發生同一個線程總是拿到同一把鎖,在嘗試拿另一把鎖時因為拿不到,而將本來已經持有的鎖釋放的過程。
解決辦法:
每個線程休眠隨機數,錯開拿鎖的時間。
線程不工作了,但是整個程序還是活着的。