第十三章、 併發死鎖問題與企業級解決方案(死鎖、活鎖、飢餓)

1、死鎖是什麼?有什麼危害?

1.1 什麼是死鎖?

  • 發生在【併發】中
  • 【互不相讓】:當兩個(或更多)線程(或進程)相互持有對方所需要的資源,又不主動釋放,導致所有人都無法繼續前進,導致程序陷入無盡的阻塞,這就是死鎖。 
  • 多個線程造成死鎖的情況(A->B->C->A)

1.2 死鎖的影響

死鎖的影響在不同系統中是不一樣的,這取決於系統對死鎖的處理能力

  • 數據庫中:檢測到死鎖,(兩個事務AB相互競爭),會放棄其中一個事務A,讓B先執行,然後再執行A
  • JVM中:無法自動處理

1.3 機率不高但危害大

  • 不一定發生,但遵循“墨菲定律”(如果事情有變壞的可能,不管這種可能性有多小,它總會發生)
  • 一旦發生,多是【高併發】場景,影響用戶多
  • 整個系統崩潰、子系統崩潰、性能降低
  • 壓力測試無法找出所有潛在的死鎖

2、發生死鎖的例子

2.1 最簡單的情況

代碼

/**
 * MustDeadLock
 *
 * @author venlenter
 * @Description: 必定發生死鎖的情況
 * @since unknown, 2020-06-10
 */
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
//線程一直不解釋,處於死鎖狀態

分析

  • T1和T2【互相等待】,都需要對方鎖定的資源才能繼續執行,從而死鎖
  • 強制中止程序,IDEA會打印多一行(code -1)
flag = 1
flag = 0

Process finished with exit code -1
  • 非0是不正常退出的信號,正常結束的程序的【結束信號是0】

2.2 實際生產中的例子:轉賬

  • 需要兩把鎖
  • 獲取兩把鎖成功,且餘額大於0,則扣除轉出人,增加收款人的餘額,是原子操作
  • 順序相反導致死鎖
/**
 * TransferMoney
 *
 * @author venlenter
 * @Description: 轉賬時候遇到死鎖,一旦打開註釋,便會發生死鎖
 * @since unknown, 2020-06-13
 */
public class TransferMoney implements Runnable {
    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的餘額" + a.balance);
        System.out.println("b的餘額" + b.balance);
    }

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

    public static void transferMoney(Account from, Account to, int amount) {
        synchronized (from) {
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("餘額不足,轉賬失敗");
                }
                from.balance -= amount;
                to.balance += amount;
                System.out.println("成功轉賬" + amount + "元");
            }
        }
    }

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

        int balance;
    }
}

//輸出結果(沒有死鎖的情況)
成功轉賬200元
成功轉賬200元
a的餘額500
b的餘額500

//開啓備註的Thread.sleep則a和b線程死鎖,沒有輸出,都在相互等待

2.3 模擬多人隨機轉賬

  • 5W人很多,但是依然會發生死鎖,墨菲定律
  • 發生死鎖機率不高但危害大
/**
 * MultiTransferMoney
 *
 * @author venlenter
 * @Description: 多人同時轉賬,依然很危險
 * @since unknown, 2020-06-13
 */
public class MultiTransferMoney {
    private static final int NUM_ACCOUNTS = 500;
    private static final int NUM_MONEY = 1000;
    private static final int NUM_THREADS = 20;
    private static int NUM_ITERATIONS = 1000000;

    public static void main(String[] args) {
        Random rnd = new Random();
        Account[] accounts = new Account[NUM_ACCOUNTS];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new 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();
        }
    }
}
//輸出結果(輸出到一定時間後,20個線程都卡住了,死鎖)
成功轉賬568元
成功轉賬129元
成功轉賬225元
成功轉賬623元
...
成功轉賬889元
餘額不足,轉賬失敗
成功轉賬451元
餘額不足,轉賬失敗
成功轉賬138元
//所有線程卡住

3、死鎖的4個必要條件(缺一不可)

  • 互斥條件(線程A拿到了鎖lock-a,則其他線程要獲取lock-a時只能等待)
  • 請求與保持條件(線程A在請求lock-b的時候,同時保持着lock-a鎖)
  • 不剝奪條件(線程A持有lock-a,外界不能剝奪A對lock-a的持有)
  • 循環等待條件(多個線程形成環路,A等待B,B等待C,C等待A)

4、如何定位死鎖

4.1 使用java命令jstack(${JAVA_HOME}/bin/jstack pid)

  • 例子
1 package ConcurrenceFolder.mooc.threadConcurrencyCore.deadlock;
2 
3 /**
4 * MustDeadLock
5 *
6 * @author venlenter
7 * @Description: 必定發生死鎖的情況
8 * @since unknown, 2020-06-10
9 */
10 public class MustDeadLock implements Runnable {
11    int flag = 1;
12    static Object o1 = new Object();
13    static Object o2 = new Object();
14
15    public static void main(String[] args) {
16        MustDeadLock r1 = new MustDeadLock();
17        MustDeadLock r2 = new MustDeadLock();
18        r1.flag = 1;
19        r2.flag = 0;
20        Thread t1 = new Thread(r1);
21        Thread t2 = new Thread(r2);
22        t1.start();
23        t2.start();
24    }
25    @Override
26    public void run() {
27        System.out.println("flag = " + flag);
28        if (flag == 1) {
29            synchronized (o1) {
30                try {
31                    Thread.sleep(500);
32                } catch (InterruptedException e) {
33                    e.printStackTrace();
34                }
35                synchronized (o2) {
36                    System.out.println("線程1成功拿到兩把鎖");
37                }
38            }
39        }
40        if (flag == 0) {
41            synchronized (o2) {
42                try {
43                    Thread.sleep(500);
44                } catch (InterruptedException e) {
45                    e.printStackTrace();
46                }
47                synchronized (o1) {
48                    System.out.println("線程2成功拿到兩把鎖");
49                }
50            }
51        }
52    }
53}

  • 查到上面執行的程序的進程pid,執行:D:\Program Files\Java\jdk1.8.0_172\bin>jstack.exe 108352
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001be53948 (object 0x0000000780caf9a0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001a93cd18 (object 0x0000000780caf9b0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at ConcurrenceFolder.mooc.threadConcurrencyCore.deadlock.MustDeadLock.run(MustDeadLock.java:48)
        - waiting to lock <0x0000000780caf9a0> (a java.lang.Object)
        - locked <0x0000000780caf9b0> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at ConcurrenceFolder.mooc.threadConcurrencyCore.deadlock.MustDeadLock.run(MustDeadLock.java:36)
        - waiting to lock <0x0000000780caf9b0> (a java.lang.Object)
        - locked <0x0000000780caf9a0> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
  • Thread1 lock f9b0,waiting f9a0
  • Thread0 lock f9a0,waiting f9b0
  • 同時也顯示了死鎖的位置MustDeadLock.java:48和MustDeadLock.java:36

4.2 ThreadMXBean代碼檢測

**
 * MustDeadLock
 *
 * @author venlenter
 * @Description: 用ThreadMXBean檢測死鎖
 * @since unknown, 2020-06-10
 */
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("發現死鎖:" + 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 = 1
flag = 0
發現死鎖:Thread-1
發現死鎖:Thread-0

5、修復死鎖的策略

5.1 線上發生死鎖應該怎麼辦?

  • 線上問題都需要防患於未然,不造成損失地撲滅幾乎已經是不可能
  • 保存案發現場然後立刻重啓服務器
  • 暫時保證線上服務的安全,然後再利用剛纔保存的信息,排查死鎖,修改代碼,重新發版

5.2 常見修復策略

  • 避免策略:【哲學家就餐】的換手方案、轉賬換序方案(思路:避免相反的獲取鎖的順序)
  • 檢測與恢復策略:一段時間檢測是否有死鎖,如果有就剝奪某個資源,來打開死鎖

5.2.1 轉賬時避免死鎖(轉賬換序方案)

  • 實際上不在乎獲取鎖的順序
  • 代碼演示
  • 通過【hashcode】來決定獲取鎖的順序、衝突時需要“加時賽”
/**
 * TransferMoney
 *
 * @author venlenter
 * @Description: 轉賬時通過【hashcode】來決定獲取鎖的順序,避免死鎖
 * @since unknown, 2020-06-13
 */
public class TransferMoney implements Runnable {
    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的餘額" + a.balance);
        System.out.println("b的餘額" + b.balance);
    }

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

    public static void transferMoney(Account from, Account to, int amount) {
        //增加內部類
        class Helper {
            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("餘額不足,轉賬失敗");
                }
                from.balance -= amount;
                to.balance += amount;
                System.out.println("成功轉賬" + amount + "元");
            }
        }
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        //通過通過【hashcode】來決定獲取鎖的順序
        if (fromHash < toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        } else {
            //當hashcode相同的時候,衝突時需要“加時賽”,用額外的lock鎖
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }

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

        int balance;
    }
}
  • 如果實體有【主鍵】就更方便

5.2.2 哲學家就餐問題

(1)問題描述

  • 流程
①先拿起左手的筷子
②然後拿起右手的筷子
③如果筷子被人使用了,那就等別人用完
④喫完後,把筷子放回原位

(2)有【死鎖】和【資源耗盡】的風險

  • 死鎖:每個哲學家都拿着左手的筷子,【永遠都在等右邊】的筷子(或相反)

(3)代碼演示:哲學家進入死鎖

/**
 * DiningPhilosophers
 *
 * @author venlenter
 * @Description: 演示哲學家就餐問題導致的死鎖
 * @since unknown, 2020-06-14
 */
public class DiningPhilosophers {
    public static class Philosopher implements Runnable {
        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Pick up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % philosophers.length];
            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();

        }
    }
}
//輸出結果
哲學家1號 Thinking
哲學家2號 Thinking
哲學家3號 Thinking
哲學家4號 Thinking
哲學家5號 Thinking
哲學家2號 Picked up left chopstick
哲學家1號 Picked up left chopstick
哲學家5號 Picked up left chopstick
哲學家3號 Picked up left chopstick
哲學家4號 Picked up left chopstick
//程序卡住,死鎖

(4)多種解決方案

  • 服務員檢查(避免策略):服務員檢查是否會陷入死鎖,如果檢查可能存在,則讓你先停止請求喫飯
  • 【改變一個哲學家拿筷子的順序(避免策略)】
  • 餐票(避免策略):事先提供允許喫飯的餐票,只有拿到餐票的纔可以執行喫飯
  • 領導調節(檢測與恢復策略);讓程序正常執行,當發現死鎖的時候,有一個外部指令進來中止其中一個線程,相當於破壞掉死鎖的“不剝奪條件”(線程A持有lock-a,外界不能剝奪A對lock-a的持有)

(5)代碼演示:解決死鎖

  • 【改變一個哲學家拿筷子的順序(避免策略)】
/**
 * DiningPhilosophers
 *
 * @author venlenter
 * @Description: 演示哲學家就餐問題導致的死鎖
 * @since unknown, 2020-06-14
 */
public class DiningPhilosophers {
    public static class Philosopher implements Runnable {
        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Pick up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % philosophers.length];
            //改進:當是最後一個哲學家,則反過來,先取右邊的筷子
            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
            new Thread(philosophers[i], "哲學家" + (i + 1) + "號").start();
        }
    }
}
//輸出結果
//長時間打印,沒有處於死鎖狀態

5.2.3 死鎖檢測與恢復策略

(1)檢測算法:鎖的調用鏈路圖

  • 允許發生死鎖
  • 每次調用鎖都記錄
  • 定期檢查“鎖的調用鏈路圖”中是否存在環路
  • 一旦發生死鎖,就用死鎖恢復機制進行恢復

(2)恢復方法1:【進程中止】

  • 【逐個終止】線程,直到死鎖消除
  • 終止順序
①優先級(是前臺交互還是後臺處理)
②已佔用資源、還需要的資源(還需要一點資源就可以完成任務的,則優先執行,終止其他的)
③已運行時間(已運行較長時間,快要完成任務的,則優先執行,終止其他的)

(3)恢復方法2:資源搶佔

  • 把已經分發出去的鎖給【收回來】
  • 讓線程【回退幾步】,這樣就不用結束整個線程,【成本比較低】
  • 缺點:可能同一個線程一直被搶佔,那就造成【飢餓】

6、實際工程中如何避免死鎖

6.1 設置【超時】時間

  • Lock的tryLock(long timeout, TimeUnit unit)
  • synchronized不具備嘗試鎖的能力
  • 造成超時的可能性多:發生了死鎖、線程陷入死循環、線程執行很慢
  • 獲取鎖失敗時:打印錯誤日誌、發報警郵件、重啓等
/**
 * TryLockDeadlock
 *
 * @author venlenter
 * @Description: 用tryLock來避免死鎖
 * @since unknown, 2020-06-14
 */
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失敗,已重試,釋放鎖1");
                            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失敗,已重試,釋放鎖2");
                            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失敗,已重試,釋放鎖1
線程2獲取到了鎖1
線程2成功獲取到了兩把鎖,釋放全部鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了兩把鎖,釋放全部鎖

6.2 多使用【併發類】而不是自己設計鎖

  • ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
  • 實際應用中java.util.concurrent.atomic十分有用,簡單方便且效率比使用Lock更高
  • 多用【併發集合】少用同步集合(Collections.synchronizedMap()和Collections.synchronizedList()),併發集合比同步集合的可擴展性更好
  • 併發場景需要用到map,首先想到用【ConcurrentHashMap】

6.3 儘量降低鎖的使用【粒度】:用不同的鎖而不是一個鎖

6.4 如果能使用【同步代碼塊】,就不使用同步方法:方便自己指定鎖對象,而不是直接整個方法

6.5 給線程起一個有意義的名字:debug和排查時事半功倍,框架和JDK都遵循這個最佳實踐

6.6 避免鎖的【嵌套】:MustDeadLock類

synchronized(lock1) {
    synchronized(lock2) {
	    //xxx
	}
}

6.7 分配資源前先看下能不能收回來:銀行家算法

6.8 儘量不要幾個功能用同一把鎖:【專鎖專用】

7、其他活性故障

  • 死鎖是最常見的活躍性問題,不過除了剛纔的死鎖之外,還有一些類似的問題,會導致程序無法順利執行,統稱爲活躍性問題
  • 【活鎖(LiveLock)】
  • 【飢餓】

7.1 活鎖

7.1.1 什麼是活鎖

  • 雖然線程並沒有阻塞,也【始終在運行】(所以叫做“活”鎖,線程是“活”的),但程序卻【得不到進展】,因爲線程始終重複做同樣的事(一直詢問請求對方的鎖)(同時佔用着CPU)
  • 如果是死鎖,那麼就是阻塞,相互等待(不佔用CPU)
  • 死鎖和活鎖的【結果是一樣的】,就是相互等待着

7.1.2 代碼演示

/**
 * LiveLock
 *
 * @author venlenter
 * @Description: 演示活鎖問題
 * @since unknown, 2020-06-15
 */
public class LiveLock {
    static class Spoon {
        private Diner owner;

        public Spoon(Diner owner) {
            this.owner = owner;
        }

        public Diner getOwner() {
            return owner;
        }

        public void setOwner(Diner owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            System.out.printf("%s喫完了!", owner.name);
        }
    }

    static class Diner {
        private String name;
        private boolean isHunger;

        public Diner(String name) {
            this.name = name;
            isHunger = true;
        }

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHunger) {
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                if (spouse.isHunger) {
                    System.out.println(name + " : 親愛的" + spouse.name + "你先喫吧");
                    spoon.setOwner(spouse);
                    continue;
                }
                spoon.use();
                isHunger = false;
                System.out.println(name + " : " + "我喫完了");
                spoon.setOwner(spouse);
            }
        }
    }

    public static void main(String[] args) {
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("織女");
        Spoon spoon = new Spoon(husband);
        new Thread(() -> husband.eatWith(spoon, wife)).start();
        new Thread(() -> wife.eatWith(spoon, husband)).start();
    }
}
//輸出結果
牛郎 : 親愛的織女你先喫吧
織女 : 親愛的牛郎你先喫吧
牛郎 : 親愛的織女你先喫吧
織女 : 親愛的牛郎你先喫吧
...//一直循環交替輸出,不停止
牛郎 : 親愛的織女你先喫吧
織女 : 親愛的牛郎你先喫吧
牛郎 : 親愛的織女你先喫吧
織女 : 親愛的牛郎你先喫吧

7.1.3 如何解決活鎖問題

  • 原因:重試機制不變,消息隊列始終重試,【喫飯始終謙讓】
  • 以太網的指數【退避】算法:雙方以隨機時間等待後再重試,不會因爲再次同時碰撞
  • 加入【隨機】因素
/**
 * LiveLock
 *
 * @author venlenter
 * @Description: 演示活鎖問題
 * @since unknown, 2020-06-15
 */
public class LiveLock {
    static class Spoon {
        private Diner owner;

        public Spoon(Diner owner) {
            this.owner = owner;
        }

        public Diner getOwner() {
            return owner;
        }

        public void setOwner(Diner owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            System.out.printf("%s喫完了!", owner.name);
        }
    }

    static class Diner {
        private String name;
        private boolean isHunger;

        public Diner(String name) {
            this.name = name;
            isHunger = true;
        }

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHunger) {
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                Random random = new Random();
                //加入隨機因素
                if (spouse.isHunger && random.nextInt(10) < 9) {
                    System.out.println(name + " : 親愛的" + spouse.name + "你先喫吧");
                    spoon.setOwner(spouse);
                    continue;
                }
                spoon.use();
                isHunger = false;
                System.out.println(name + " : " + "我喫完了");
                spoon.setOwner(spouse);
            }
        }
    }

    public static void main(String[] args) {
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("織女");
        Spoon spoon = new Spoon(husband);
        new Thread(() -> husband.eatWith(spoon, wife)).start();
        new Thread(() -> wife.eatWith(spoon, husband)).start();
    }
}
//輸出結果
牛郎 : 親愛的織女你先喫吧
織女 : 親愛的牛郎你先喫吧
牛郎 : 親愛的織女你先喫吧
織女喫完了!織女 : 我喫完了
牛郎喫完了!牛郎 : 我喫完了

7.1.4 工程中的活鎖實例:【消息隊列】

  • 錯誤方法:消息處理失敗時,如果放到隊列開頭重試,當服務出了問題,處理該消息一直失敗,則會導致程序一直卡着
  • 解決:【將失敗的消息放到隊列尾部】、重試限制(比如限制重連3次,超過3次就做其他的邏輯)

7.2 飢餓

  • 當線程需要某些資源(例如CPU),但卻【始終得不到】
  • 線程的【優先級】設置得過於低(如設置爲1),或者有線程持有鎖同時又無限循環從而【不釋放鎖】,或者某程序【始終佔用】某文件的【寫鎖】
  • 飢餓可能會導致【響應性差】:比如,瀏覽器有A線程負責前臺響應(打開收藏夾等動作),B線程負責後臺下載圖片和文件、計算渲染等。如果後臺線程B把CPU資源都佔用了,那麼前臺線程A將無法得到很好地執行,這會導致用戶體驗很差

8、常見面試問題

(1)寫一個【必然死鎖】的例子,生產中什麼場景下會發生死鎖?

  • 例子:線程設置flag區分啓動,相互調用對方的鎖(AB,BA)
  • 什麼場景下會發生死鎖:相互調用鎖
/**
 * MustDeadLock
 *
 * @author venlenter
 * @Description: 必定發生死鎖的情況
 * @since unknown, 2020-06-10
 */
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成功拿到兩把鎖");
                }
            }
        }
    }
}

2、發生死鎖必須滿足【哪些條件】?

  • 互斥條件(線程A拿到了鎖lock-a,則其他線程要獲取lock-a時則只能等待)
  • 請求與保持條件(線程A在請求lock-b的時候,同時保持着lock-a鎖)
  • 不剝奪條件(線程A持有lock-a,外界不能剝奪A對lock-a的持有)
  • 循環等待條件(多個線程形成環路,A等待B,B等待C,C等待A)

3、如何【定位】死鎖

  • jstack:發生死鎖後,通過pid dump出線程詳情
  • ThreadMXBean:代碼中檢測

4、有哪些【解決】死鎖問題的【策略】?

  • 避免策略:【哲學家就餐】的換手方案(最後一個人切換方向)、轉賬換序方案(通過【hashcode】來決定獲取鎖的順序)
  • 檢測與恢復策略:一段時間【檢測】是否有死鎖,如果有就【剝奪】某個資源,來打開死鎖
  • 鴕鳥策略:不推薦

5、講一講經典的【哲學家就餐】問題

  • 解決方案

6、實際工程中如何【避免死鎖】?

  • ①設置【超時】時間
  • ②多使用【併發類】而不是自己設計鎖
  • ③儘量降低鎖的使用【粒度】:用不同的鎖而不是一個鎖
  • ④如果能使用【同步代碼塊】,就不使用同步方法:方便自己指定鎖對象,而不是直接整個方法
  • ⑤給線程起一個有意義的名字:debug和排查時事半功倍,框架和JDK都遵循這個最佳實踐
  • ⑥避免鎖的【嵌套】:MustDeadLock類
  • ⑦分配資源錢先看下能不能收回來:銀行家算法
  • ⑧儘量不要幾個功能用同一把鎖:【專鎖專用】

7、什麼是活躍性問題?活鎖、飢餓和死鎖有什麼區別?

 

筆記來源:慕課網悟空老師視頻《Java併發核心知識體系精講》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章