3死锁从产生到消除.png

思维导图

死锁

什么是死锁

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

截屏2020-01-23下午2.17.36.png

多个线程造成死锁的情况

如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能发生死锁
截屏2020-01-23下午2.20.39.png

死锁的影响

死锁的影响在不同的系统中是不一样的,这取决于系统对死锁的处理能力
数据库中:检测并放弃事务
JVM中:无法自动处理

几率不高但危害大

不一定发生,但是遵守“墨菲定律”
一旦发生,多是高并发场景,影响用户多
整个系统崩溃,子系统崩溃,性能降低
压力测试,无法找出所有潜在的死锁

死锁的四个必要条件

互斥条件
请求与保持条件
不剥夺条件
循环等待

如何定位死锁

jstack

ThreadMXBean

  1. /**
  2. * 描述: 用ThreadMXBean检测死锁
  3. */
  4. public class ThreadMXBeanDetection implements Runnable {
  5. int flag = 1;
  6. static Object o1 = new Object();
  7. static Object o2 = new Object();
  8. public static void main(String[] args) throws InterruptedException {
  9. ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
  10. ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
  11. r1.flag = 1;
  12. r2.flag = 0;
  13. Thread t1 = new Thread(r1);
  14. Thread t2 = new Thread(r2);
  15. t1.start();
  16. t2.start();
  17. Thread.sleep(1000);
  18. ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
  19. long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
  20. if (deadlockedThreads != null && deadlockedThreads.length > 0) {
  21. for (int i = 0; i < deadlockedThreads.length; i++) {
  22. ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
  23. System.out.println("发现死锁" + threadInfo.getThreadName());
  24. }
  25. }
  26. }
  27. @Override
  28. public void run() {
  29. System.out.println("flag = " + flag);
  30. if (flag == 1) {
  31. synchronized (o1) {
  32. try {
  33. Thread.sleep(500);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. synchronized (o2) {
  38. System.out.println("线程1成功拿到两把锁");
  39. }
  40. }
  41. }
  42. if (flag == 0) {
  43. synchronized (o2) {
  44. try {
  45. Thread.sleep(500);
  46. } catch (InterruptedException e) {
  47. e.printStackTrace();
  48. }
  49. synchronized (o1) {
  50. System.out.println("线程2成功拿到两把锁");
  51. }
  52. }
  53. }
  54. }
  55. }

修复死锁

线上发生死锁怎么办

线上问题都是防患于未然,不造成损失的扑灭几乎是不可能
保存案发现场后立即重启服务器
暂时保证线上服务器的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发版

常见的修复策略

避免策略:哲学家就餐的换手方案、转账换序方案

实际上不在乎获取锁的顺序
通过hashcode来决定获取锁的顺序、冲突时需要“另加一把锁”(不一定是hash值,惟一的主键更加方便,惟一目的是给锁排序)

检测与恢复:一段时间检测是否有死锁,如果有就剥夺某一资源,来打开死锁

检测方法:锁的调用链图
恢复方法1: 进程终止
恢复方法2: 资源抢占

鸵鸟策略:我们发生死锁的概率极低,那么我们就直接忽略它,直到死锁发生时,再人工修复


哲学家就餐问题

截屏2020-01-23下午3.48.15.png

哲学家就餐-流程

  1. 先拿起左手的筷子
  2. 然后拿起右手的筷子
  3. 如果筷子被人使用了,那就等别人用完
  4. 吃完后把筷子放回原位

有死锁和资源耗尽的风险

死锁:每个哲学家都拿左手的餐叉,永远都在等右边的餐叉

死锁代码

  1. /**
  2. * 描述: 演示哲学家就餐问题导致的死锁
  3. */
  4. public class DiningPhilosophers {
  5. public static class Philosopher implements Runnable {
  6. private Object leftChopstick;
  7. public Philosopher(Object leftChopstick, Object rightChopstick) {
  8. this.leftChopstick = leftChopstick;
  9. this.rightChopstick = rightChopstick;
  10. }
  11. private Object rightChopstick;
  12. @Override
  13. public void run() {
  14. try {
  15. while (true) {
  16. doAction("Thinking");
  17. synchronized (leftChopstick) {
  18. doAction("Picked up left chopstick");
  19. synchronized (rightChopstick) {
  20. doAction("Picked up right chopstick - eating");
  21. doAction("Put down right chopstick");
  22. }
  23. doAction("Put down left chopstick");
  24. }
  25. }
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. private void doAction(String action) throws InterruptedException {
  31. System.out.println(Thread.currentThread().getName() + " " + action);
  32. Thread.sleep((long) (Math.random() * 10));
  33. }
  34. }
  35. public static void main(String[] args) {
  36. Philosopher[] philosophers = new Philosopher[5];
  37. Object[] chopsticks = new Object[philosophers.length];
  38. for (int i = 0; i < chopsticks.length; i++) {
  39. chopsticks[i] = new Object();
  40. }
  41. for (int i = 0; i < philosophers.length; i++) {
  42. Object leftChopstick = chopsticks[i];
  43. Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
  44. philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
  45. new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
  46. }
  47. }
  48. }

多种解决方案

服务员检查(避免)
改变一个哲学家拿叉子的顺序(避免)
5个人只给4张餐票永远有一人在等待(避免)
领导调节(检测与恢复策略)

改变一个哲学家拿叉子的顺序代码,避免死锁

  1. /**
  2. * 描述: 演示哲学家就餐问题导致的死锁
  3. */
  4. public class DiningPhilosophers {
  5. public static class Philosopher implements Runnable {
  6. private Object leftChopstick;
  7. public Philosopher(Object leftChopstick, Object rightChopstick) {
  8. this.leftChopstick = leftChopstick;
  9. this.rightChopstick = rightChopstick;
  10. }
  11. private Object rightChopstick;
  12. @Override
  13. public void run() {
  14. try {
  15. while (true) {
  16. doAction("Thinking");
  17. synchronized (leftChopstick) {
  18. doAction("Picked up left chopstick");
  19. synchronized (rightChopstick) {
  20. doAction("Picked up right chopstick - eating");
  21. doAction("Put down right chopstick");
  22. }
  23. doAction("Put down left chopstick");
  24. }
  25. }
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }
  29. }
  30. private void doAction(String action) throws InterruptedException {
  31. System.out.println(Thread.currentThread().getName() + " " + action);
  32. Thread.sleep((long) (Math.random() * 10));
  33. }
  34. }
  35. public static void main(String[] args) {
  36. Philosopher[] philosophers = new Philosopher[5];
  37. Object[] chopsticks = new Object[philosophers.length];
  38. for (int i = 0; i < chopsticks.length; i++) {
  39. chopsticks[i] = new Object();
  40. }
  41. for (int i = 0; i < philosophers.length; i++) {
  42. Object leftChopstick = chopsticks[i];
  43. Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
  44. if (i == philosophers.length - 1) {
  45. philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
  46. } else {
  47. philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
  48. }
  49. new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
  50. }
  51. }
  52. }

避免死锁

设置超时时间

Lock的tryLock(long timeout,TimeUnit unit)
Synchronized不具备尝试获取锁的能力
造成超时的可能性多:发生了死锁、线程陷入了死循环、线程执行很慢
获取锁失败:打日志、上报报警

多使用并发类而不是设计锁

ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
实际应用中java.util.concurrent.atomic十分有用,简单方便切效率比使用Lock更高
多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好

尽量降低锁的使用粒度:用不同锁而不是一个锁

如果能使用同步代码块,就不使用同步方法:自己指定锁对象

给你的线程起一个有意义的名字:debug和排查时事半功倍

避免锁的嵌套:MustDeadLock类

分配资源前先看能不能收回来:银行家算法

尽量不要几个功能用一把锁:专锁专用

活锁

什么是活锁

虽然线程没有阻塞,也始终在运行(所以叫做“活”锁,线程是“活”的),但程序却得不到进展,因为线程始终在做同样的事

解决活锁

如何解决活锁

原因:重试机制不变,消息队列始终重试
以太网的指数退避算法
加入随机因素

工程中的活锁实例:消息队列

策略:消息如果处理失败,就放在队列头重试
由于依赖服务出现问题,处理该消息一直失败
没有阻塞,但程序无法继续
解决:放在队列尾、重试机制

饥饿

当线程需要某些资源(例如CPU),但是却始终得不到
线程的优先级设置得过低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁
饥饿回导致响应性差