【Java并发编程】深入理解死锁问题及其解决方案

苡仁 2020年04月12日 39次浏览

1. 死锁的定义与影响

1.1 什么是死锁

  • 发生在并发中,多个线程(进程)互不相让,相互持有对方所以需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,就是死锁。

  • 如上图两个人,红色球员说:你先给我球我就放手;蓝色球员说:你先放开我我就给你球
  • 专业一点如下图:

  • 多个线程(大于2)死锁如何发生

    假设有三个线程:线程1持有锁A想要获取锁B;线程2持有锁B想要获取锁C;线程3持有锁C想要获取锁A;三者形成了一个环,谁也不先主动释放锁,谁也获取不到需要的锁,就形成了多个线程之间的死锁。

1.2 死锁的影响

  • 死锁在不同系统中是不一样的,不一定死锁就一定有危害,取决于系统对死锁的处理能力。
  1. 数据库:某些数据库可以检测并放弃死锁。
  2. JVM:无法自动处理,但是可以检测。
  • JVM死锁的发生几率不高但是危害很大,一旦发生,很多都是高并发场景,如果系统崩溃,非常影响用户。此外并发压力测试无法全部找出潜在的死锁。

2. 死锁的例子

2.1 死锁简单案例

  • 代码案例就如上面两个线程相互等待的图一样,thread1获取了object1的锁,thread2获取了object2的锁,然后分别想要获取到对方的锁,由于两个线程互不相让,使得程序运行一直不会停止
/**
 * @author yiren
 */
public class DeadLockMust {
    private static Object object1 = new Object();
    private static Object object2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (object1) {
                System.out.println(Thread.currentThread().getName() + " object1");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                    System.out.println(Thread.currentThread().getName() + " object2");
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (object2) {
                System.out.println(Thread.currentThread().getName() + " object2");
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1) {
                    System.out.println(Thread.currentThread().getName() + " object1");
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
Thread-1 object2
Thread-0 object1

  • 如果IDEA强行点停止,退出编码会变为:130, 而正常的退出编码为0, 提示如下
Process finished with exit code 130 (interrupted by signal 2: SIGINT)

2.2 转账案例

  • 两人互相转账,对转入转出账户加锁,形成相互依赖,然后发生死锁。
/**
 * 两人互相转账
 * @author yiren
 */
public class DeadlockTransferMoney {

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread thread1 = new Thread(() -> {
            try {
                transferMoney(a, b,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                transferMoney(b, a, 200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("a: " + a);
        System.out.println("b: " + b);
    }


    public static void transferMoney(Account from, Account to, Integer amount) throws InterruptedException {
        synchronized (from) {
            TimeUnit.MILLISECONDS.sleep(500);
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("余额不足!转账失败!");
                    return;
                }
                from.balance -= amount;
                to.balance += amount;
                System.out.println("转账成功,转账:" + amount + "元");
            }
        }
    }
}
  • 这个程序和上面的线程死锁的案例代码原理是一样的。

2.3 多人随机转账案例

  • 案例实现:随机向某个用户转账,并对转入转出账户先加锁。同时启N个线程
/**
 * @author yiren
 */
public class DeadlockMultiTransferMoney {
    private static final int ACCOUNTS_NUM = 50;
    private static final int MONEY_NUM = 1000;
    private static final int ITERATIONS_NUM = 1000000;
    private static final int THREAD_NUM = 20;
    private static Account[] accounts = new Account[ACCOUNTS_NUM];

    static {
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new Account(MONEY_NUM);
        }
    }


    public static void main(String[] args) {
        for (int i = 0; i < THREAD_NUM; i++) {
            new TransferThread().start();
        }

    }

    private static class TransferThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < ITERATIONS_NUM; i++) {
                int fromAccount = new Random().nextInt(ACCOUNTS_NUM);
                int toAccount = new Random().nextInt(ACCOUNTS_NUM);
                int amount = new Random().nextInt(MONEY_NUM);
                try {
                    transferMoney(accounts[fromAccount], accounts[toAccount], amount);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void transferMoney(Account from, Account to, Integer amount) throws InterruptedException {
        synchronized (from) {
            TimeUnit.MILLISECONDS.sleep(100);
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("余额不足!转账失败!");
                    return;
                }
                from.balance -= amount;
                to.balance += amount;
                System.out.println("转账成功,转账:" + amount + "元");
            }
        }
    }
}
  • 程序启动后,运行一段时间输出就会停止,虽然两个账户相互转账在众多账户中是小概率事件,但是还是会发生,互相转账形成死锁。
  • 不仅是相互转账会发生死锁,还有如果N个人形成了一个环路,此时也会形成死锁。回顾一下上面说的3个线程的情况,把锁A B C换成三个账户即可。

假设有三个线程:线程1持有锁A想要获取锁B;线程2持有锁B想要获取锁C;线程3持有锁C想要获取锁A;三者形成了一个环,谁也不先主动释放锁,谁也获取不到需要的锁,就形成了多个线程之间的死锁。

3. 死锁的4个必要条件

  1. 互斥条件:一个资源每一次只能被一个线程使用。

  2. 请求与保持条件:一个线程持有第一把锁,又去请求第二把锁,然后第二把锁拿不到,就等待了,第一把锁也没释放掉。

  3. 不剥夺条件:多线程竞争资源,相互持有对方所以需要的资源时,线程不被外部因素干扰剥夺其竞争的权利。

  4. 循环等待条件:多线程竞争资源,相互持有对方所以需要的资源时,构成一个环路(循环依赖),不可解开。

4. 定位死锁

4.1 jstack定位死锁详解

  1. 以上面的DeadlockMust为例
  • 我们先找出pid --> 使用jps 命令
> jps
52049 Launcher
52050 DeadlockMust
52106 Jps
46876
...
  • 然后使用jstack pid
> jstack 52050
2020-02-14 19:15:57
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.241-b07 mixed mode):

"Attach Listener" #14 daemon prio=9 os_prio=31 tid=0x00007fdfbe840000 nid=0xa403 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #13 prio=5 os_prio=31 tid=0x00007fdfc1099800 nid=0x1003 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
.......
.......
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fdfbd011f78 (object 0x000000076ac2a7e8, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fdfbd014758 (object 0x000000076ac2a7f8, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at com.imyiren.concurrency.deadlock.DeadlockMust.lambda$main$1(DeadlockMust.java:35)
	- waiting to lock <0x000000076ac2a7e8> (a java.lang.Object)
	- locked <0x000000076ac2a7f8> (a java.lang.Object)
	at com.imyiren.concurrency.deadlock.DeadlockMust$$Lambda$2/2129789493.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
"Thread-0":
	at com.imyiren.concurrency.deadlock.DeadlockMust.lambda$main$0(DeadlockMust.java:22)
	- waiting to lock <0x000000076ac2a7f8> (a java.lang.Object)
	- locked <0x000000076ac2a7e8> (a java.lang.Object)
	at com.imyiren.concurrency.deadlock.DeadlockMust$$Lambda$1/1607521710.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
  • 中间的部分信息省去。
  • 我们先看Found one Java-level deallock: 告诉我们发现一个死锁。然后下面告诉我们Thread-1和Thread-0分别等待那个锁,这个锁被谁持有。如下:Thread-1在等待0x00007fdfbd011f78Thread-0持有
"Thread-1":
  waiting to lock monitor 0x00007fdfbd011f78 (object 0x000000076ac2a7e8, a java.lang.Object),
  which is held by "Thread-0"
  • 然后我们找到Java stack information for the threads listed above:下面的信息
  • Thread-1- waiting to lock <0x000000076ac2a7e8>等待****7e8的锁locked <0x000000076ac2a7f8> 持有****7f8
"Thread-1":
	at com.imyiren.concurrency.deadlock.DeadlockMust.lambda$main$1(DeadlockMust.java:35)
	- waiting to lock <0x000000076ac2a7e8> (a java.lang.Object)
	- locked <0x000000076ac2a7f8> (a java.lang.Object)
	at com.imyiren.concurrency.deadlock.DeadlockMust$$Lambda$2/2129789493.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
  • Thread-10:- waiting to lock <0x000000076ac2a7f8>等待****7f8的锁locked <0x000000076ac2a7e8>持有****7e8`
"Thread-0":
	at com.imyiren.concurrency.deadlock.DeadlockMust.lambda$main$0(DeadlockMust.java:22)
	- waiting to lock <0x000000076ac2a7f8> (a java.lang.Object)
	- locked <0x000000076ac2a7e8> (a java.lang.Object)
	at com.imyiren.concurrency.deadlock.DeadlockMust$$Lambda$1/1607521710.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
  1. 我们按照思路再看下上面DeadlockMultiTransferMoney类的死锁,直接看jstack信息
Found one Java-level deadlock:
=============================
"Thread-19":
  waiting to lock monitor 0x00007fc7f401a738 (object 0x000000076ac2f280, a com.imyiren.concurrency.deadlock.Account),
  which is held by "Thread-1"
"Thread-1":
  waiting to lock monitor 0x00007fc7f700d4b8 (object 0x000000076ac2f1e0, a com.imyiren.concurrency.deadlock.Account),
  which is held by "Thread-15"
"Thread-15":
  waiting to lock monitor 0x00007fc7f700e7f8 (object 0x000000076ac2f3a0, a com.imyiren.concurrency.deadlock.Account),
  which is held by "Thread-8"
"Thread-8":
  waiting to lock monitor 0x00007fc7f700d408 (object 0x000000076ac2f460, a com.imyiren.concurrency.deadlock.Account),
  which is held by "Thread-5"
"Thread-5":
  waiting to lock monitor 0x00007fc7f8016ff8 (object 0x000000076ac2f270, a com.imyiren.concurrency.deadlock.Account),
  which is held by "Thread-18"
"Thread-18":
  waiting to lock monitor 0x00007fc7f700d408 (object 0x000000076ac2f460, a com.imyiren.concurrency.deadlock.Account),
  which is held by "Thread-5"

Java stack information for the threads listed above:
===================================================
"Thread-19":
	at com.imyiren.concurrency.deadlock.DeadlockMultiTransferMoney.transferMoney(DeadlockMultiTransferMoney.java:49)
	- waiting to lock <0x000000076ac2f280> (a com.imyiren.concurrency.deadlock.Account)
	at com.imyiren.concurrency.deadlock.DeadlockMultiTransferMoney$TransferThread.run(DeadlockMultiTransferMoney.java:38)
"Thread-1":
	at com.imyiren.concurrency.deadlock.DeadlockMultiTransferMoney.transferMoney(DeadlockMultiTransferMoney.java:51)
	- waiting to lock <0x000000076ac2f1e0> (a com.imyiren.concurrency.deadlock.Account)
	- locked <0x000000076ac2f280> (a com.imyiren.concurrency.deadlock.Account)
	at com.imyiren.concurrency.deadlock.DeadlockMultiTransferMoney$TransferThread.run(DeadlockMultiTransferMoney.java:38)
....
....

Found 1 deadlock.
  • 注意:jstack 是不一定能准确得告诉我们发生了死锁的,如果遇到jstack分析不出来的,我们可以通过信息自行分析。

4.2 代码定位死锁 --- ThreadMXBean


/**
 * 使用ThreadMXBean检测死锁
 * 使用DeadlockMust改造
 * @author yiren
 */
public class ThreadMxBeanDetection {
    private static Object object1 = new Object();
    private static Object object2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (object1) {
                System.out.println(Thread.currentThread().getName() + " object1");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object2) {
                    System.out.println(Thread.currentThread().getName() + " object2");
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (object2) {
                System.out.println(Thread.currentThread().getName() + " object2");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (object1) {
                    System.out.println(Thread.currentThread().getName() + " object1");
                }
            }
        });
        thread1.start();
        thread2.start();

        // 等待死锁发生
        TimeUnit.SECONDS.sleep(2);

        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        if (null != deadlockedThreads && deadlockedThreads.length > 0) {
            for (int i = 0; i < deadlockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
                System.out.println("found deadlock thread: " + threadInfo.getThreadName());
            }
        }
    }
}
Thread-0 object1
Thread-1 object2
found deadlock thread: Thread-1
found deadlock thread: Thread-0

5. 修复死锁的策略

5.1 生产环境发生死锁怎么办?

  • 一般线上发生死锁,不可提前预料,且蔓延非常快,影响特别大,所以我们需要防范于未然。

  • 如果真的发生了,首先应该保存案发现场,然后迅速恢复服务。恢复后再排查修复问题。

5.2 修复策略一:避免策略

  • 哲学家就餐的换手方案,转账更换顺序方案
  1. 哲学家就餐问题:wiki:哲学家就餐问题点击查看
  • 死锁的发生:每个哲学家都拿起左边的餐具,再去拿右边的餐具,就陷入了等待。
/**
 * 哲学家问题
 *
 * @author yiren
 */
public class DiningPhilosophers {

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] objects = new Object[philosophers.length];
        for (int i = 0; i < objects.length; i++) {
            objects[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            philosophers[i] = new Philosopher(objects[i], objects[(i+1)%objects.length]);
            new Thread(philosophers[i], "philosopher-" + (i + 1)).start();
        }
    }


    private static class Philosopher implements Runnable {
        private Object left;
        private Object right;

        public Philosopher(Object left, Object right) {
            this.left = left;
            this.right = right;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    // thinking...
                    action("thinking...");
                    synchronized (left) {
                        // Pick up left.
                        action("Pick up left ");
                        synchronized (right) {
                            // Pick up right and eating
                            action("Pick up right and eating ");
                            action("Put down right");
                        }
                        action("Put down left");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        private void action(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " do " + action);
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10));
        }
    }
}
  • 避免策略解决:

    • 找服务员做协调,来分发餐具
    • 改变一个哲学家拿餐具的顺序
    • 给一个餐具减一数量的信号量相当于餐票
  • 检测与恢复策略:

    • 第三方敢于,剥夺某个哲学家的吃饭条件
  • 改变一个哲学家拿餐具的顺序

    我们可以替换创建哲学家的时候的一个人左右顺序,修改上mian函数代码如下:

    
        public static void main(String[] args) {
            Philosopher[] philosophers = new Philosopher[5];
            Object[] objects = new Object[philosophers.length];
            for (int i = 0; i < objects.length; i++) {
                objects[i] = new Object();
            }
            for (int i = 0; i < philosophers.length; i++) {
                if (philosophers.length - 1 == i) {
                    philosophers[i] = new Philosopher(objects[(i + 1) % objects.length], objects[i]);
                } else {
                    philosophers[i] = new Philosopher(objects[i], objects[(i + 1) % objects.length]);
                }
                new Thread(philosophers[i], "philosopher-" + (i + 1)).start();
            }
        }
    
  1. 相互转账问题:我们可以把上面两个转账时获取锁的顺序给修正一下,让相互转账获取锁的顺序一致。代码如下:
/**
 * 两人互相转账 修复死锁
 *
 * @author yiren
 */
public class DeadlockTransferMoneyFix {

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread thread1 = new Thread(() -> {
            try {
                transferMoney(a, b, 100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                transferMoney(b, a, 200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("a: " + a);
        System.out.println("b: " + b);
    }


    public static void transferMoney(Account from, Account to, Integer amount) throws InterruptedException {
        int fromHashCode = from.hashCode();
        int toHashCode = to.hashCode();
        if (fromHashCode > toHashCode) {
            synchronized (from) {
                synchronized (to) {
                    transfer(from, to, amount);
                }
            }
            return;
        } else if (fromHashCode < toHashCode) {
            synchronized (to) {
                synchronized (from) {
                    transfer(from, to, amount);
                }
            }
        } else {
            synchronized (lock) {
                transfer(from, to, amount);
            }
        }
    }

    private static void transfer(Account from, Account to, Integer amount) {
        if (from.balance - amount < 0) {
            System.out.println("余额不足!转账失败!");
            return;
        }
        from.balance -= amount;
        to.balance += amount;
        System.out.println("转账成功,转账:" + amount + "元");
    }
}
转账成功,转账:100元
转账成功,转账:200元
a: Account{balance=1100}
b: Account{balance=900}

Process finished with exit code 0
  • 代码中使用hashCode来排序,因为hashCode有重复,所以我们需要处理等于的情况,但在业务中我们也可以使用账户唯一ID之类的来做一个排序

5.3 修复策略二:检测恢复策略

  • 定时检测是否有死锁,如果有就剥夺某个资源以打开死锁

  • 检测算法:锁的调用链路图

    1. 允许发生死锁

    2. 每次调用所都记录

    3. 定期检查“锁的调用链路图”中是否存在环路

    4. 如果存在就意味着发生了死锁

    5. 然后我们就用指定的恢复策略去解决死锁

  • 恢复策略:

    1. 进程终止:
      • 逐个终止线程,直到死锁消除
      • 终止顺序:考虑(1)优先级 (2)占用资源比 (3)运行时间
    2. 资源抢占:
      • 分发出去的锁给收回来
      • 让线程回退几步,不用整体结束线程,成本低
      • 缺点:可能造成某些线程一直得不到运行,产生饥饿

5.4 修复策略三:鸵鸟策略

  • 鸵鸟策略就是之当鸵鸟遇到危险和困难时,总是把自己的头埋在沙子里,一动也不动,就是掩耳盗铃的意思。等同于我们如果知道发生死锁的概率极低,我们可以直接忽略它,等到它发生了,再人工修复。

6. 避免死锁的发生

6.1 方式一:设置超时时间

  • Lock的tryLock(long timeout, TimeUnit unit)

  • synchronized不具备这样的尝试获取锁能力

  • 造成超时的可能性很多如:发生死锁、死循环、代码执行慢

  • 无论如何,我们只要超时时间到了,就认为是获取锁失败,然后进行失败处理,打日志、报警、重启等等

/**
 * 使用tryLock来避免死锁
 *
 * @author yiren
 */
public class DeadlockTryLock {
    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                        System.out.println(Thread.currentThread().getName() + " got lock 1");
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10));
                        if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                            System.out.println(Thread.currentThread().getName() + " got lock1 and lock2 successfully.");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println(Thread.currentThread().getName() + " fail to get lock2");
                            lock1.unlock();
                        }
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10));
                    } else {
                        System.out.println(Thread.currentThread().getName() + " fail to get lock1");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                        System.out.println(Thread.currentThread().getName() + " got lock 2");
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10));
                        if (lock1.tryLock(1, TimeUnit.SECONDS)) {
                            System.out.println(Thread.currentThread().getName() + " got lock2 and lock1 successfully.");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println(Thread.currentThread().getName() + " fail to get lock1");
                            lock2.unlock();
                        }
                        TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10));
                    } else {
                        System.out.println(Thread.currentThread().getName() + " fail to get lock2");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
Thread-0 got lock 1
Thread-1 got lock 2
Thread-1 fail to get lock1
Thread-0 got lock1 and lock2 successfully.
Thread-1 got lock 2
Thread-1 got lock2 and lock1 successfully.

Process finished with exit code 0

6.2 使用并发类

  • `ConcurrentHashMap、ConcurrentLinkedQueue等

  • java.util.concurrent.aotmic.*下的原子类也是简单好用效率也高

  • 优先使用并发集合而不是同步集合

6.3 其他的一些点

  • 我们在加锁的时候,尽量降低锁的颗粒度,用不同的锁做不同的事情,不用一个锁

  • 能用同步代码块就不用同步方法,并且自己指定锁对象,方便自己控制

  • 尽量给线程命名,使其有意义,方便排查问题。

  • 我们应该尽量避免锁的嵌套,锁的嵌套容易引起死锁

  • 分配资源前,考虑能不能收回来:银行家算法

  • 尽量不要多个功能使用同一把锁:专锁专用

7. 其他活性故障

  • 死锁是最常见的活跃性问题,除了死锁外,还有一些类似的问题也会导致程序无法成功执行,统称为活跃性问题。也就是活锁、饥饿

7.1 活锁(LiveLock)

  • 什么是活锁?

    活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

    • 就比如:哲学家问题,先同时拿起左边的餐具,然后等待5分钟,又同时放下,再等待五分钟,又同时去尝试拿去左边的餐具.....; 我们可以把等待的事件设置成随机数,就可以解决

    虽然线程没有阻塞,始终在运行,但是程序得不到进展,线程始终在重复同样的事情

  • 代码演示:

/**
 * 活锁问题
 *
 * @author yiren
 */
public class LiveLock {

    private static class Spoon {
        private Diner owner;

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

        public synchronized void use() {
            System.out.println(owner.name + " has eaten!");
        }
    }

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

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

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHungry) {
                if (spoon.owner != this) {
                    try {
                        TimeUnit.MILLISECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }

                if (spouse.isHungry) {
                    System.out.println(name + " : " + spouse.name + " first!");
                    spoon.owner = spouse;
                    continue;
                }

                spoon.use();
                isHungry = false;
                System.out.println(name + " : finished!");
                spoon.owner = spouse;
            }
        }
    }

    public static void main(String[] args) {
        Diner man = new Diner("Man");
        Diner woman = new Diner("Women");

        Spoon spoon = new Spoon(man);

        Thread manThread = new Thread(() -> {
            man.eatWith(spoon,woman);
        });
        Thread womanThread = new Thread(() -> {
            woman.eatWith(spoon,man);
        });
        manThread.start();
        womanThread.start();
    }
}
Women : Man first!
Man : Women first!
Women : Man first!
Man : Women first!
Women : Man first!
Man : Women first!
Women : Man first!
Man : Women first!
Women : Man first!
Man : Women first!
Women : Man first!
Man : Women first!
Women : Man first!
Man : Women first!
Women : Man first!
Man : Women first!
.......
  • 互相谦让,始终循环在此逻辑里面。怎么解决呢?

    加入随机因素,让重试策略更加合理,修改上面的代码中的eatWith方法

    
            public void eatWith(Spoon spoon, Diner spouse) {
                while (isHungry) {
                    if (spoon.owner != this) {
                        try {
                            TimeUnit.MILLISECONDS.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        continue;
                    }
    
                    Random random = new Random();
                    if (spouse.isHungry && random.nextInt(10) < 7) {
                        System.out.println(name + " : " + spouse.name + " first!");
                        spoon.owner = spouse;
                        continue;
                    }
    
                    spoon.use();
                    isHungry = false;
                    System.out.println(name + " : finished!");
                    spoon.owner = spouse;
                }
            }
        }
    
    
    Man : Women first!
    Women : Man first!
    Man has eaten!
    Man : finished!
    Women has eaten!
    Women : finished!
    
    Process finished with exit code 0
    
    

    除此之外,我们还可以增加重试机制。如果失败多次过后,使用补偿策略处理。

7.2 饥饿

  • 当线程需要某些资源,但始终得不到(如CPU资源),饥饿会导致响应性能变差,需要等待很久甚至超时失败。
  • 例如:线程的优先级过低、线程持有锁同时又无限循环不释放锁、程序始终占用某资源的写锁等
  • 程序设计不应该依赖于优先级
    • 线程优先级在不同操作系统不一样,在Java中设置的优先级可能在不同平台上对应的不一样
    • 此外,操作系统可能会改变线程的优先级,使得其优先级变低等

8. 面试常考问题

  1. 写一个必然死锁的例子
  2. 发生死锁必须满足哪些条件
  3. 如何定位死锁
  4. 有哪些解决死锁问题的策略?
  5. 讲讲经典的哲学家就餐问题
  6. 实际工程中如何避免死锁
  7. 什么是活跃性问题?活锁、饥饿和死锁有什么区别?

  • 文章内容来源:
  • 《Java并发编程艺术》、JDK1.8版本源码、慕课网悟空JUC课程