文章目錄
- 關于死鎖
- 一.死鎖的三種情況
- 1.一個線程,一把鎖,連續多次加鎖
- 2.兩個線程,兩把鎖
- 3.N個線程,M把鎖 --哲學家就餐問題
- 二.如何避免死鎖
- 死鎖是如何構成的(四個必要條件)
- 打破死鎖
- 三.死鎖小結
關于死鎖
一.死鎖的三種情況
- 1.一個線程,一把鎖,連續多次加鎖 -->由synchronized 鎖解決
- 2.兩個線程,兩把鎖,每個線程獲取到一把鎖之后,嘗試獲取對方的鎖
- 3.N個線程,M把鎖 -->一個經典的模型,哲學家就餐問題
1.一個線程,一把鎖,連續多次加鎖
一個線程,一把鎖,連續多次加鎖,在實際學習和工作中,是很容易被寫出來的,一旦方法調用的層次比較深,就搞不好容易出現這樣的情況,想要解除阻塞,需要 往下執行,想要往下執行,就需要等待第一次的鎖被釋放,這樣就形成了死鎖(dead lock),就如同下面的Demo18,一個線程對同一把鎖進行多次加鎖,但是運行出來結果沒錯
為了解決當方法調用層次比較深出現一個線程,一把鎖,多次加鎖形成死鎖的情況,Java中的synchronized 就引入了可重入概念,在上一篇博客 synchronized關鍵字里有詳細解釋,本篇博客不再贅述
代碼示例:
class Counter{private int count = 0;synchronized public void add(){count++;}public int get(){return count;}public synchronized static void func(){synchronized (Counter.class){}}}
public class Demo18 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Counter counter = new Counter();Thread t1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+counter.get());}
}
2.兩個線程,兩把鎖
兩個線程,兩把鎖,每個線程獲取到一把鎖之后,嘗試獲取對方的鎖
用生活中的實際場景,舉例說明:
比如,吃餃子~~,朝新喜歡蘸醬油吃,小舟喜歡蘸醋吃,后來兩人都習慣了對方的習慣,兩人都是同時蘸醋和醬油吃餃子,朝新拿起醬油,小舟拿起醋
朝新說:你把醋給我,我用完了,全都給你
小舟說:不行,你把醬油先給我,我用完了,全都給你
此時兩個線程互不相讓,就會構成死鎖~~
還比如,房鑰匙鎖車里了,車鑰匙鎖家里了
代碼示例:
public class Demo20 {public static void main(String[] args) throws InterruptedException {Object lock1 = new Object();Object lock2 = new Object();Thread t1 =new Thread(() ->{synchronized (lock1){//朝新拿起醬油try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//朝新嘗試拿起醋synchronized (lock2){System.out.println("t1 線程兩個鎖都獲取到");}}});Thread t2 =new Thread(() ->{synchronized (lock2){//小舟拿起醋try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//小舟嘗試拿起醬油synchronized (lock1){System.out.println("t2 線程兩個鎖都獲取到");}}});t1.start();t2.start();t1.join();t2.join();}
}
必須是,拿到第一把鎖,再拿第二把鎖(不能釋放第一把鎖)
其中加入sleep的作用:
加入sleep就是為了確保上述錯誤代碼構成死鎖
如果讓我們手寫一個出現死鎖的代碼,就是要通過上述代碼,寫兩個線程兩把鎖,注意要精確控制好加鎖的順序,不進行控制的話,隨機調度就有可能不構成死鎖了
3.N個線程,M把鎖 --哲學家就餐問題
大部分情況下,上述模型,可以很好的運轉,但是在一些極端情況下會造成死鎖
像是,同一時刻,大家都想吃面條,同時拿起左手的筷子,此時任何一個線程都無法拿起右手的筷子,任何一個哲學家都吃不成面條
每個線程,都不會放下手里的筷子,而是阻塞等待,構成死鎖
上述場景雖說非常極端,但是在以后的學習和工作中,比如我們以后會做服務器開發,同時為很多個用戶提供服務,假設上述場景,即使出現死鎖的概率是1%%,服務器可能一天要處理幾千萬的請求(比如百度,一天要處理10億量級的請求),這樣就會出現10萬次死鎖情況,就比如溫總理說的:在咱們國家,再小的問題,乘以13億都是大問題~~,那么如何避免死鎖問題呢?
二.如何避免死鎖
死鎖是如何構成的(四個必要條件)
- 1.鎖是互斥的,一個線程拿到鎖之后,另一個線程再嘗試獲取鎖,必須要阻塞等待 (鎖的基本性質)
- 2.鎖是不可搶占的(不可剝奪),線程1拿到鎖 線程2也嘗試獲取這個鎖,線程2必須阻塞等待,而不是線程2直接把鎖搶過來 (鎖的基本特性)
- 3.請求和保持,一個線程拿到鎖1 之后不釋放鎖1的前提下,去獲取鎖2
- 4.循環等待,多個線程,多把鎖之間的等待過程,構成了"循環",A等待B,B等待C,C等待A
以上四個形成死鎖的必要條件,其中1和2都是鎖自己的基本性質和特性,至少,Java中的synchronized鎖是遵守這兩點的,各種語言中內置的鎖/主流的鎖,都是遵守這兩點的,這兩點我們改變不了
只要破壞上述的3 ,4任何一個條件都能夠打破死鎖
打破死鎖
- 1.打破必要條件3 :請求和保持
如果是先放下左手的筷子,再去拿右手的筷子,就不會構成死鎖了,也就是代碼中加鎖的時候,不要"嵌套加鎖"
代碼示例:
public class Demo20 {public static void main(String[] args) throws InterruptedException {Object lock1 = new Object();Object lock2 = new Object();Thread t1 =new Thread(() ->{synchronized (lock1){//朝新拿起醬油try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//朝新嘗試拿起醋synchronized (lock2){System.out.println("t1 線程兩個鎖都獲取到");}});Thread t2 =new Thread(() ->{synchronized (lock2){//小舟拿起醋try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}//小舟嘗試拿起醬油synchronized (lock1){System.out.println("t2 線程兩個鎖都獲取到");}});t1.start();t2.start();t1.join();t2.join();}
}
這種破壞死鎖的方法不夠通用,有些情況下,確實需要拿到多個鎖,再進行某個操作的(嵌套,很難避免)
- 2.打破必要條件4 :循環等待
約定好加鎖的順序,就可以破除循環等待了,我們約定好,每個線程加鎖的時候,永遠是先獲取序號小的鎖,后獲取序號大的鎖
通過上述哲學家就餐模型,我們可以觀察到,只要規定好加鎖的順序,就可以打破循環等待,從而避免死鎖問題
我們使用上述吃餃子過程中出現的死鎖問題來觀察,通過破除循環等待,也就是規定好加鎖順序后,是如何避免死鎖問題的
public class Demo20 {public static void main(String[] args) throws InterruptedException {Object lock1 = new Object();Object lock2 = new Object();Thread t1 =new Thread(() ->{synchronized (lock1){//朝新拿起醬油System.out.println("t1 拿到locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//朝新嘗試拿起醋synchronized (lock2){System.out.println("t1 線程兩個鎖都獲取到 吃面條");}}});Thread t2 =new Thread(() ->{synchronized (lock1){//小舟拿起醋System.out.println("t2 拿到locker1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//小舟嘗試拿起醬油synchronized (lock2){System.out.println("t2 線程兩個鎖都獲取到 吃面條");}}});t1.start();t2.start();t1.join();t2.join();}
}