1.什么是“死锁”?
在多线程并发中,两个及以上线程互相持有对方所需要的资源又不主动释放,导致程序进入无尽的阻塞这就是“死锁”;
2.写一段“死锁”代码
import java.util.concurrent.TimeUnit;
/*** 写一段必然发生死锁代码*/
public class MustDeadLock implements Runnable{// 定义标记位,用于指示不同的线程获取不同的锁int flag = 1;// 定义两把锁static Object lockOne = new Object();static Object lockTwo = new Object();@Overridepublic void run() {if(flag == 1){synchronized (lockOne){ // 获取锁“lockOne”System.out.println("线程一获取到了锁lockOne");try {TimeUnit.MILLISECONDS.sleep(500); // 休眠的意义在于等待两个线程都已成功获取了“第一把锁”} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockTwo){ // 获取锁“lockOne”成功后,尝试获取锁“lockTwo”System.out.println("线程一获取到了两把锁...");}}}if(flag == 2){synchronized (lockTwo){ // 获锁“lockTwo”System.out.println("线程二获取到了锁lockTwo");try {TimeUnit.MILLISECONDS.sleep(500); // 休眠的意义在于等待两个线程都已成功获取了“第一把锁”} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockOne){ // 获取锁“lockTwo”成功后,尝试获取锁“lockOne”System.out.println("线程二获取到了两把锁...");}}}}// 测试public static void main(String[] args) {MustDeadLock r1 = new MustDeadLock();MustDeadLock r2 = new MustDeadLock();r1.flag = 1; // 设置标记位r2.flag = 2; // // 设置标记位Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();}
}
3.如何定位“死锁”
方式一:使用Java自带的jstack命令
使用前提:已知死锁可能发生在哪个类中;
流程:
1.运行上面的死锁代码,让程序先出现死锁问题;
2.进入程序所使用JDK的bin目录,执行jps获取到其进程号;
3.再执行jstack "pid" 就可以查找到
方式二:使用ThreadMXBean定位死锁
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.TimeUnit;
/*** 使用ThreadMXBean检测死锁*/
public class ThreadMXBeanDetection implements Runnable{// 定义标记位,用于指示不同的线程获取不同的锁int flag = 1;// 定义两把锁static Object lockOne = new Object();static Object lockTwo = new Object();@Overridepublic void run() {if(flag == 1){synchronized (lockOne){ // 获取锁“lockOne”System.out.println("线程一获取到了锁lockOne");try {TimeUnit.MILLISECONDS.sleep(500); // 休眠的意义在于等待两个线程都已成功获取了“第一把锁”} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockTwo){ // 获取锁“lockOne”成功后,尝试获取锁“lockTwo”System.out.println("线程一获取到了两把锁...");}}}if(flag == 2){synchronized (lockTwo){ // 获锁“lockTwo”System.out.println("线程二获取到了锁lockTwo");try {TimeUnit.MILLISECONDS.sleep(500); // 休眠的意义在于等待两个线程都已成功获取了“第一把锁”} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockOne){ // 获取锁“lockTwo”成功后,尝试获取锁“lockOne”System.out.println("线程二获取到了两把锁...");}}}}// 测试public static void main(String[] args) throws InterruptedException {ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();r1.flag = 1; // 设置标记位r2.flag = 2; // // 设置标记位Thread t1 = new Thread(r1,"线程一");Thread t2 = new Thread(r2,"线程二");t1.start();t2.start();TimeUnit.MILLISECONDS.sleep(500);// 此处开始使用ThreadMXBean对象检测“死锁”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]); // 线程信息String threadName = threadInfo.getThreadName();System.out.println("发生“死锁”的线程名字为:" + threadName);// TODO 当然已经检测到发生“死锁”问题,那你可以发送邮件给开发者让其处理,也可以让其自动重启;}}}
}
4.修复“死锁”的策略
1.鸵鸟策略
说明:如果发生“死锁”的概率极低,那我们就直接忽略它,直到“死锁”发生时再去处理它;
2.避免策略
说明:“哲学家就餐例子”换手方案;
3.检测与恢复策略
说明:允许发生“死锁”,发生死锁时“我”帮你检测出来并恢复;
检测算法:锁的调用链路图;
恢复方法一:进程终止(逐个终止线程直到死锁消除);
根据我们自己判断的优先级去终止一些对业务影响较小且造成死锁的线程;
恢复方法二:资源抢占(把发出去的锁收回来,让线程回退几步);
缺点:可能同一个线程一直被抢占造成该线程一直无法获取到锁“饥饿”问题;
5.实际开发中如何避免“死锁”
1.设置超时时间
2.多使用并发类,而不是自己去设计锁
ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等...
3.降低锁的粒度尽量不要几个功能用同一把锁(专锁专用)
4.能使用同步代码块就不使用同步方法
使用同步代码块可以自己指定锁对象,锁的范围小于等于同步方法;
5.给线程起一个有意义的名字
如果发现“死锁”或线程安全问题时,我们根据线程名去排查问题时就可以事半功倍;
6.避免锁的嵌套
7.分配“锁”资源前看能不能收回
“银行家算法”,分配锁之前看能不能收回锁假如分配锁后能收回锁,就算发生死锁我们
也可以让线程执行回退两步回收锁,避免死锁;
6.什么是“活锁”?
活锁表示执行任务的线程没有被阻塞,但由于某些条件没有被满足导致重复的去尝试执行任务(失败->尝试->失败),“活锁”与“死锁”的区别在于“死锁”线程一直处于等待状态而“活锁”执行线程在不断的执行;
7.如何解决“活锁”
加入随机因素;加入随机数让线程双方不再让渡资源优先随机使其一方先执行后再释放锁;
活锁案例:活锁——牛郎织女的幸福生活_牛郎织女幸福生活-CSDN博客文章浏览阅读692次,点赞4次,收藏6次。从前,有一对非常恩爱的夫妻,他们都有一颗谦让的心,但家境却不是很好,吃了上顿没下顿,于是,当他们有食物的时候,他们会优先考虑对方,如果对方饿的话,就让给对方吃,等对方吃饱了自己才吃,这种美德本身是好的,但是如果一味的谦让,也会发生一些让人啼笑皆非的事儿…_牛郎织女幸福生活https://blog.csdn.net/qq_36221788/article/details/103078399 “消息队列”就是一个活锁例子,如果消息处理失败就放在队列开头重试,由于服务出现问题导
致该消息一直重试失败;
8.什么是“饥饿”问题?有什么影响?
“饥饿”是指线程执行时无法获取到它所需要的资源比如(CPU),一种情况是“线程”优先级分配不合理导致部分线程一直无法被CPU调度执行;另一种情况是某线程持有某个操作的“锁”但其又处于无限循环而又不释放锁,此时其它等待获取此锁的线程获取不到就出现了饥饿问题;饥饿问题会导致服务器响应变差;
9.面试题:
1.写一个必然发生“死锁”的例子
详见本章序号2;
2.生产中什么场景会发生“死锁”
一个方法中获取多把锁时易发生死锁问题;或循环调用获取锁会形成一个闭合的死锁链路;
比如库存的增减,金额的增减;
3.死锁的四个必要条件
1.互斥(资源只能同时被一个线程竞争获取)
2.某线程已持有一把锁在不释放已获取到的锁,同时再去不停的获取另一把锁;
3.其它线程不能释放这个线程已获取到的锁;
4.循环等待(多线程中循环等待构成环路才会发生死锁)
4.如何定位“死锁”
详见本章序号3;
5.有哪些解决“死锁”问题的策略
详见本章序号4;
6.实际工作中如何避免死锁发生
详见本章序号5;
7.什么是活跃性问题?活锁,死锁,饥饿有什么区别?
详见本章序号1、7、8;
悟空老师Java并发编程思维导图:
https://naotu.baidu.com/file/ec7748c253f4fc9d88ac1cc1e47814f3