Java并發(fā)編程 死鎖與修復死鎖

1.死鎖是什么扯罐?有什么危害戒悠?

1.1 什么是死鎖
  • 發(fā)生在并發(fā)中
  • 互不相讓:當兩個(或更多)線程(或進程)相互持有對方所需要的資源,又不主動釋放讥此,導致所有人都無法繼續(xù)前進,導致程序陷入無盡的阻塞谣妻,這就是死鎖萄喳。
image.png
  • 多個線程造成死鎖的情況
image.png
1.2 死鎖的影響

死鎖的影響在不同系統(tǒng)中是不一樣的,這取決于系統(tǒng)對死鎖的處理能力

  • 數(shù)據(jù)庫中:檢測并放棄事務
  • JVM中:無法自動處理
幾率不高但危害大
  • 不一定發(fā)生蹋半,但是遵守"墨菲定律"
  • 一旦發(fā)生他巨,多是高并發(fā)場景,影響用戶多
  • 整個系統(tǒng)崩潰、子系統(tǒng)崩潰染突、性能降低
  • 壓力測試無法找出所有潛在的死鎖
1.3 發(fā)生死鎖的例子
必定發(fā)生死鎖的情況
/**
 * 描述:     必定發(fā)生死鎖的情況
 */
public class MustDeadLock implements Runnable {

    int flag = 1;

    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("線程1成功拿到兩把鎖");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("線程2成功拿到兩把鎖");
                }
            }
        }
    }
}
flag = 1
flag = 0
多個人相互轉賬
  • 需要兩把鎖
  • 獲取兩把鎖成功捻爷,且余額大于0,則扣除轉出人觉痛,增加收款人的余額役衡,是原子操作
  • 順序相反導致死鎖
public class MultiTransferMoney {

    private static final int NUM_ACCOUNTS = 500;
    private static final int NUM_MONEY = 1000;
    private static final int NUM_ITERATIONS = 1000000;
    private static final int NUM_THREADS = 100;

    public static void main(String[] args) {

        Random rnd = new Random();
        TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new TransferMoney.Account(NUM_MONEY);
        }
        class TransferThread extends Thread {

            @Override
            public void run() {
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int toAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int amount = rnd.nextInt(NUM_MONEY);
                    TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
                }
                System.out.println("運行結束");
            }
        }
        for (int i = 0; i < NUM_THREADS; i++) {
            new TransferThread().start();
        }
    }
}
public class TransferMoney implements Runnable {

    static Account a = new Account(500);
    static Account b = new Account(500);
    static Object lock = new Object();
    int flag = 1;

    public static void transferMoney(Account from, Account to, int amount) {
        class Helper {

            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("余額不足,轉賬失敗薪棒。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功轉賬" + amount + "元");
            }
        }

        synchronized (from) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (to) {
                new Helper().transfer();
            }
        }
    }

    @Override
    public void run() {
        if (flag == 1) {
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            transferMoney(b, a, 200);
        }
    }

    static class Account {

        int balance;

        public Account(int balance) {
            this.balance = balance;
        }
    }
}

死鎖

image.png

程序停止輸出發(fā)生死鎖

2. 死鎖的4個必要條件:

  1. 互斥條件
  2. 請求與保持條件
  3. 不剝奪條件
  4. 循環(huán)等待條件

3. 如何定位死鎖

3.1.jstack定位死鎖

并發(fā)編程中的死鎖定位排查

image.png
image.png
3.2.ThreadMXBean定位死鎖
public class ThreadMXBeanDetection implements Runnable {

    int flag = 1;

    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
        ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            for (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("發(fā)現(xiàn)死鎖" + threadInfo.getThreadName());
            }
        }
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("線程1成功拿到兩把鎖");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("線程2成功拿到兩把鎖");
                }
            }
        }
    }
}
flag = 0
flag = 1
發(fā)現(xiàn)死鎖Thread-1
發(fā)現(xiàn)死鎖Thread-0

4. 修復死鎖策略

線上發(fā)生死鎖應該什么辦

  • 線上問題都需要防患與未然手蝎,不造成損失幾乎是不可能
  • 保存死鎖數(shù)據(jù),然后立刻重啟服務器
  • 暫時保證線上服務的安全俐芯,然后再利用剛才保存的信息棵介,排查死鎖,修改代碼吧史,重新發(fā)版

常見修復策略

4.1. 避免策略
示例1 修改兩人轉賬時獲取鎖的順序

經(jīng)過思考邮辽,我們可以發(fā)現(xiàn),其實轉賬時贸营,并不在乎兩把鎖的相對獲取順序吨述。轉賬的時候,我們無論先獲取到轉出賬戶鎖對象钞脂,還是先獲取到轉入賬戶鎖對象揣云,只要最終能拿到兩把鎖,就能進行安全的操作冰啃。所以我們來調整一下獲取鎖的順序邓夕,使得先獲取的賬戶和該賬戶是“轉入”或“轉出”無關,而是使用 HashCode 的值來決定順序阎毅,從而保證線程安全

    public static void transferMoney(Account from, Account to, int amount) {
        class Helper {

            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("余額不足焚刚,轉賬失敗。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功轉賬" + amount + "元");
            }
        }
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        if (fromHash < toHash) {
            synchronized (from) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        }
        else if (fromHash > toHash) {
            synchronized (to) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        }else  {
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }

可以看到扇调,我們會分別計算出這兩個 Account 的 HashCode矿咕,然后根據(jù) HashCode 的大小來決定獲取鎖的順序。這樣一來肃拜,不論是哪個線程先執(zhí)行痴腌,不論是轉出還是被轉入,它獲取鎖的順序都會嚴格根據(jù) HashCode 的值來決定燃领,那么大家獲取鎖的順序就一樣了士聪,就不會出現(xiàn)獲取鎖順序相反的情況,也就避免了死鎖

總結:通過hashcode來決定獲取鎖的順序猛蔽、沖突時需要“加時賽”(再加一把鎖)獲取鎖
image.png
示例2 哲學家換手解決.改變一個哲學家拿筷子的順序.
/**
 * 描述:     演示哲學家就餐問題導致的死鎖
 */
public class DiningForPhilosophers {

    /**
     * 哲學家類
     */
    public static class Philosophers implements Runnable{

        private Object leftChopsticks;
        private Object rightChopsticks;

        public Philosophers(Object leftChopsticks, Object rightChopsticks) {
            this.leftChopsticks = leftChopsticks;
            this.rightChopsticks = rightChopsticks;
        }

        @Override
        public void run() {
            try {
                while(true){
                    action("思考......");
                    synchronized (leftChopsticks){
                        action("拿起左邊的筷子");
                        synchronized (rightChopsticks){
                            action("拿起右邊的筷子---吃飯");
                            action("放下右邊的筷子");
                        }
                        action("放下左邊的筷子");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public static void action(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName()+"-->"+action);
            //隨機休眠,代表這個行為執(zhí)行的耗時
            Thread.sleep((long) (Math.random()*10));
        }
    };

    public static void main(String[] args) {
        Philosophers[] philosophers = new Philosophers[5];
        Object[] chopsticks = new Object[philosophers.length];
        //初始化筷子對象
        for (int i=0;i<chopsticks.length;i++){
            chopsticks[i] = new Object();
        }
        //初始化哲學家對象,并啟動線程.
        for (int i=0;i<chopsticks.length;i++){
            Object leftChopsticks = chopsticks[i];
            Object rightChopsticks = chopsticks[(i+1)%chopsticks.length];
            philosophers[i] = new Philosophers(leftChopsticks, rightChopsticks);
            new Thread(philosophers[i],"哲學家"+i+"號").start();
        }
    }

}
image.png

同樣發(fā)生死鎖,這里大家陷入了一種循環(huán)等待的狀態(tài),0號獲取了他左邊的0號筷子,請求他右邊的1號筷子,1號獲取了左邊的1號筷子,請求等待他右邊的2號筷子...........5號獲取了他左邊的5號筷子等待他右手邊的0號筷子........這樣就形成了一個環(huán).

哲學家問題解決策略
  • 服務員檢查
    哲學家拿叉子時 服務員檢查叉子數(shù)量
  • 改變一個哲學家拿叉子的順序
  • 餐票
    哲學家就餐時先拿到餐票就能就餐
  • 領導調節(jié)(檢測與恢復策略)
這里演示改變一個哲學家拿叉子的順序修復
image.png

image.png

這樣就成功的避免了死鎖

4.2 檢測與恢復策略:一段時間檢測是否有死鎖,如果有就剝奪某個資源,來解除死鎖

允許死鎖的發(fā)生,但是發(fā)生死鎖后要記錄下來并通過停止線程或其他方式停止死鎖

死鎖檢測算法
  • 允許發(fā)生死鎖
  • 每次調用鎖的記錄
  • 定期檢查"鎖的調用鏈路圖"中是否存在環(huán)路
  • 一旦發(fā)生死鎖剥悟,就用死鎖恢復機制進行恢復機制進行恢復
鎖的調用鏈路圖
恢復方法1:進程終止
  • 逐個終止線程灵寺,直到死鎖消除。
  • 終止順序:
    1. 優(yōu)先級(重要性区岗,是前臺交互還是后臺處理)
    2. 已占用資源略板、還需要的資源
    3. 已經(jīng)運行時間
恢復方法2:資源搶占
  • 把已經(jīng)分發(fā)出去的鎖給收回來
  • 讓線程回退幾步,這樣就不用結束整個線程慈缔,成本比較低
    比如 讓哲學家把拿起的筷子再放下
  • 缺點:可能同一個線程一直被搶占叮称,那就造成饑餓
4.3 鴕鳥策略

死鎖發(fā)生的幾率特別小,忽略他,等死鎖發(fā)生了,再去處理修改

5 實際工程中如何避免死鎖

1. 設置超時時間
  • Lock的tryLock(long timeout,TimeUnit unit)
    造成超時的可能性很多,發(fā)生了死鎖,線程陷入了死循環(huán),線程執(zhí)行很慢.
/**
 * 描述:     用tryLock來避免死鎖
 */
public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println("線程1獲取到了鎖1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("線程1獲取到了鎖2");
                            System.out.println("線程1成功獲取到了兩把鎖");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println("線程1嘗試獲取鎖2失敗藐鹤,已重試");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("線程1獲取鎖1失敗瓤檐,已重試");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println("線程2獲取到了鎖2");

                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("線程2獲取到了鎖1");
                            System.out.println("線程2成功獲取到了兩把鎖");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println("線程2嘗試獲取鎖1失敗,已重試");
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("線程2獲取鎖2失敗娱节,已重試");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
線程1獲取到了鎖1
線程2獲取到了鎖2
線程1嘗試獲取鎖2失敗挠蛉,已重試
線程2獲取到了鎖1
線程2成功獲取到了兩把鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了兩把鎖
2.多使用并發(fā)類而不是自己設計鎖

如ConcurrentHashMap
java.util.concurrent.atomic.

3.盡量降低鎖的使用粒度:用不同的鎖而不是一個鎖

縮小鎖的臨界區(qū)

4.如果能使用同步代碼塊,就不使用同步方法:自己指定鎖對象

縮小了同步范圍,可以自己指定鎖對象

5.給線程指定有意義的名字,方便后期debug和排查
6.避免鎖的嵌套:MustDeadLock 演示的嵌套
7.分配資源前先看能不能收回來:銀行家算法

銀行家算法(Java實現(xiàn))

8.盡量不要幾個功能用通一把鎖:專鎖專用

特別感謝:

悟空

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肄满,隨后出現(xiàn)的幾起案子谴古,更是在濱河造成了極大的恐慌,老刑警劉巖稠歉,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掰担,死亡現(xiàn)場離奇詭異,居然都是意外死亡怒炸,警方通過查閱死者的電腦和手機恩敌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來横媚,“玉大人,你說我怎么就攤上這事月趟〉坪” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵孝宗,是天一觀的道長穷躁。 經(jīng)常有香客問我,道長因妇,這世上最難降的妖魔是什么问潭? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮婚被,結果婚禮上狡忙,老公的妹妹穿的比我還像新娘。我一直安慰自己址芯,他們只是感情好灾茁,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布窜觉。 她就那樣靜靜地躺著,像睡著了一般北专。 火紅的嫁衣襯著肌膚如雪禀挫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天拓颓,我揣著相機與錄音语婴,去河邊找鬼。 笑死驶睦,一個胖子當著我的面吹牛砰左,可吹牛的內容都是我干的。 我是一名探鬼主播啥繁,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼菜职,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了旗闽?” 一聲冷哼從身側響起酬核,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎适室,沒想到半個月后嫡意,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡捣辆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年蔬螟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汽畴。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡旧巾,死狀恐怖,靈堂內的尸體忽然破棺而出忍些,到底是詐尸還是另有隱情鲁猩,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布罢坝,位于F島的核電站廓握,受9級特大地震影響,放射性物質發(fā)生泄漏嘁酿。R本人自食惡果不足惜隙券,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望闹司。 院中可真熱鬧娱仔,春花似錦、人聲如沸开仰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恩溅,卻和暖如春隔箍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背脚乡。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工蜒滩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奶稠。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓俯艰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锌订。 傳聞我的和親對象是個殘疾皇子竹握,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容