Java并发核心知识体系精讲---死锁的前世今生
声明:本文是自己自学慕课网悟空老师的《Java并发核心知识体系精讲》的死锁部分后整理而成课程笔记。
课程链接如下:https://coding.imooc.com/class/362.html
如有侵权,请私信我并第一时间删除本文。
1. 死锁是什么?有什么危害
1.1什么是死锁
发生在并发中
互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进, 导致程序陷入无尽的阻塞,这就是死锁。
一图胜千言
多个线程造成死锁的情况
如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁
1.2 死锁的影响
死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力
◆数据库中:检测并放弃事务
◆JVM中:无法自动处理
2. 发生死锁例子
2.1 最简单的死锁
/*** 描述: 必定发生死锁的情况*/
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();}
@Overridepublic 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时(T1) ,先锁定O1,睡眠500毫秒, 然后锁定O2 ;
- 而T1在睡眠的时候另一个flag=0的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放01 ;
- T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定;
- T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定;
- T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁。
注意看退出信号: Process finished with exit code 130(interrupted by signal 2: SIGINT) ,是不正常退出的信号,对比正常结束的程序的结束信号是0。
2.2 实际生产中的例子:转账
需要两把锁
获取两把锁成功,且余额大于0 ,则扣除转出人,增加收款人的余额,是原子操作
顺序相反导致死锁
/*** 描述: 转账时候遇到死锁,一旦打开注释,便会发生死锁*/ 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);} @Overridepublic 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){/**加上sleep就造成死锁了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; } }
未加注释代码部分,运行结果如下
加上注释后运行结果如下,由于请求锁的顺序相反,造成相互等待,互不退让,造成死锁。
2.3 模拟多人随机转账
- 5万人很多,但是依然会发生死锁,墨菲定律
- 复习:发生死锁几率不高但危害大
import java.util.Random;
/*** 描述: 多人同时转账,依然很危险*/
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 = 20;
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 {@Overridepublic 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(accounts[fromAcct], accounts[toAcct], amount);}System.out.println("运行结束");}}for (int i = 0; i < NUM_THREADS; i++) {new TransferThread().start();}}public static void transferMoney(Account from, Account to, int amount) {synchronized (from){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;
}
}
运行结果,发生死锁
3. 死锁的4个必要条件(缺一不可)
1.互斥条件
每个资源每次只能被一个线程(或进程,下同)使用。
为什么资源不能同时被多个线程或进程使用呢?这是因为如果每个人都可以拿到想要的资源,那就不需要等待,所以是不可能发生死锁的。
2.请求与保持条件
一个线程因请求资源而阻塞时,则需对已获得的资源保持不放。
如果在请求资源时阻塞了,并且会自动释放手中资源(例如锁)的话,那别人自然就能拿到我刚才释放的资源,也就不会形成死锁。
3.不剥夺条件
指线程已获得的资源,在未使用完之前,不会被强行剥夺。
比如我们数据库,它就有可能去强行剥夺某一个事务所持有的资源,这样就不会发生死锁了。所以要想发生死锁,必须满足不剥夺条件,也就是说当现在的线程获得了某一个资源后,别人就不能来剥夺这个资源,这才有可能形成死锁。
4.循环等待条件
只有若干线程之间形成一种头尾相接的循环等待资源关系时,才有可能形成死锁。
比如在两个线程之间,这种“循环等待”就意味着它们互相持有对方所需的资源、互相等待;而在三个或更多线程中,则需要形成环路,例如依次请求下一个线程已持有的资源等
4. 如何定位死锁
4.1 jstack
4.2 死锁检测工具类(ThreadMXBean)
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
/*** 描述: 用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("发现死锁" + threadInfo.getThreadName());}}//以上是核心代码}
@Overridepublic 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成功拿到两把锁");}}}}
}
5. 修复死锁的策略
5.1 线上发生了死锁怎么办
线上问题都需要防患于未然,不造成损失地扑灭几乎已经是 不可能
◆保存案发现场然后立刻重启服务器
◆暂时保证线上服务的安全,然后在利用刚才保存的信息,排查死锁,修改代码,重新发版
5.2 常见的修复策略
- 死锁避免策略:哲学家就餐的换手方案、转账换序方案
- 检测与恢复策略: 一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁。
- 鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。
5.2.1 死锁避免策略(包含哲学家就餐问题)
思路:避免相反的获取锁的顺序
转账时避免死锁
实际上不在乎获取锁的顺序
代码演示
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);}@Overridepublic 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("余额不足,转账失败。");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) {synchronized (to) {new Helper().transfer();}}}else if (fromHash > toHash) {synchronized (to) {synchronized (from) {new Helper().transfer();}}}else { //通过hashcode来决定获取锁的顺序、冲突时需要“加时赛”,有主键更方便synchronized (lock) {synchronized (to) {synchronized (from) {new Helper().transfer();}}}}}
避免策略:哲学家就餐的换手方案、转账换序方案
有死锁和资源耗尽的风险
◆死锁:每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)
/*** 描述: 演示哲学家就餐问题导致的死锁*/
public class DiningPhilosophers {public static class Philosopher implements Runnable {private Object leftChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {this.leftChopstick = leftChopstick;this.rightChopstick = rightChopstick;}
private Object rightChopstick;
@Overridepublic void run() {try {while (true) {doAction("Thinking");synchronized (leftChopstick) {doAction("Picked up left chopstick");synchronized (rightChopstick) {doAction("Picked 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) % chopsticks.length]; philosophers[i] = new Philosopher(leftChopstick, rightChopstick); new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();}}
}
哲学家问题的多种解决方案
服务员检查(避免策略)
改变一个哲学家拿叉子的顺序(避免策略)
/*** 描述: 其他不变,修改main函数*/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) % chopsticks.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.2 死锁的检测与恢复策略
检测算法:锁的调用链路图
◆允许发生死锁
◆每次调用锁都记录
◆定期检查"锁的调用链路图”中是否存在环路
◆一旦发生死锁,就用死锁恢复机制进行恢复
恢复方法1:进程终止
逐个终止线程,直到死锁消除。
◆终止顺序:
1.优先级(是前台交互还是后台处理)
2.已占用资源、还需要的资源
3.已经运行时间
恢复方法2:资源抢占
◆把已经分发出去的锁给收回来
◆让线程回退几步,这样就不用结束整个线程,成本比较低
◆缺点:可能同一个线程一直被抢占,那就造成饥饿
6. 实际工程中如何避免死锁
设置超时时间
- Lock的tryLock(long timeout, TimeUnit unit)
- synchronized不具备尝试锁的能力
- 造成超时的可能性多: 发生了死锁、线程陷入死循环、线程 执行很慢
- 获取锁失败:打日志、发报警邮件、重启等
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/*** 描述: 用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();}
@Overridepublic 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();}}}}
}
2.多使用并发类而不是自己设计锁
◆ConcurrentHashMapConcurrentLinkedQueue、 AtomicBoolean等
◆实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高
◆多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好
◆并发场景需要用到map ,首先想到用ConcurrentHashMap
3.尽量降低锁的使用粒度 :用不同的锁而不是一个锁
4.如果能使用同步代码块,就不使用同步方法:自己指定锁对象
5.给你的线程起个有意义的名字: debug和排查时事半功倍,框架 和JDK都遵守这个最佳实践
6.避免锁的嵌套: MustDeadLock类
7.分配资源前先看能不能收回来:银行家算法
8.尽量不要几个功能用同一把锁:专锁专用
7. 其他活性故障(又叫活跃性问题)
◆死锁是最常见的活跃性问题,不过除了刚才的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题
◆活锁( LiveLock )
有死锁和资源耗尽的风险
死锁:每个哲学家都拿着左手的餐叉,永远都在等右边的餐 叉(或者相反) 活锁:在完全相同的时刻进入餐厅,并同时拿起左边的餐叉, 那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再 等五分钟,又同时拿起这些餐叉。
在实际的计算机问题中, 缺乏餐叉可以类比为缺乏共享资源
7.1 活锁
◆什么是活锁
- 虽然线程并没有阻塞,也始终在运行(所以叫做“活”锁,线程是“活”的),但是程序却得不到进展,因为线程始终重复做同样的事
- 如果这里死锁,那么就是这里两个人都始终一动不动,直到对方先抬头,他们之间不再说话了,只是等待
- 如果发生活锁,那么这里的情况就是,双方都不停地对对方说 “你先起来吧,你先起来吧”, 双方都一直在说话,在运行
- 死锁和活锁的结果是一样的,就是谁都不能先抬头
◆代码演示
import java.util.Random;
/*** 描述: 演示活锁问题*/
public class LiveLock {//spoon为勺子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);
}}//Diner为就餐者static class Diner {private String name;private boolean isHungry;
public Diner(String name) {this.name = name;isHungry = true;}
public void eatWith(Spoon spoon, Diner spouse) {while (isHungry) {if (spoon.owner != this) {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}continue;}
if (spouse.isHungry) {System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");spoon.setOwner(spouse);continue;}
spoon.use();isHungry = 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(new Runnable() {@Overridepublic void run() {husband.eatWith(spoon, wife);}}).start();
new Thread(new Runnable() {@Overridepublic void run() {wife.eatWith(spoon, husband);}}).start();}
}
原因:重试机制不变,消息队列始终重试,吃饭始终谦让
如何解决活锁问题:
- 以太网的指数退避算法
- 加入随机因素
public void eatWith(Spoon spoon, Diner spouse) {while (isHungry) {if (spoon.owner != this) {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}continue;}//加入随机Random random = new Random();if (spouse.isHungry && random.nextInt(10) < 9) {System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");spoon.setOwner(spouse);continue;}
spoon.use();isHungry = false;System.out.println(name + ": 我吃完了");spoon.setOwner(spouse);
}}}
◆工程中的活锁实例:消息队列
- 策略:消息如果处理失败,就放在队列开头重试
- 由于依赖服务出了问题,处理该消息一直失败.
- 没阻塞,但程序无法继续
- 解决:放到队列尾部、重试限制
7.2 饥饿
8. 面试常考问题
Java并发核心知识体系精讲---死锁的前世今生相关推荐
- 线程八大核心+java并发核心知识体系精讲_Java从业者如果不懂这些,面试80%都会挂在这些核心知识上面...
JVM 无论什么级别的Java从业者,JVM都是进阶时必须迈过的坎.不管是工作还是面试中,JVM都是必考题.如果不懂JVM的话,薪酬会非常吃亏(近70%的面试者挂在JVM上了) 详细介绍了JVM有关于 ...
- java并发核心知识体系精讲_JAVA核心知识汇总
双非同学如何逆袭大厂? 在互联网行业,入行的第一份工作很大程度上决定了以后职业发展的高度.有些双非的同学认为自己校招进不了大厂以后还会有社招,这种想法很危险.大厂的社招,大多数都只招大厂的员工.什么意 ...
- java并发核心知识体系精讲_JVM核心知识体系
作者:林振华 来源:编程原理 1.问题 如何理解类文件结构布局? 如何应用类加载器的工作原理进行将应用辗转腾挪? 热部署与热替换有何区别,如何隔离类冲突? JVM如何管理内存,有何内存淘汰机制? JV ...
- java并发核心知识体系精讲_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析
版权说明:本文内容根据 github 开源项目整理所得 项目地址: https://github.com/Snailclimb/JavaGuidegithub.com 一.基础 什么是线程和进程? ...
- Java多线程,并发核心知识体系总结
目录 Java底层原理: 线程终止--interrupt 线程安全--运算结果出错, 活跃性问题:死锁.活锁.饥饿锁, 对象发布和初始化的线程安全 Java三兄弟--JVM内存结构,Java内存模型, ...
- 40000+字超强总结?阿里P8把Java全栈知识体系详解整理成这份PDF
40000 +字长文总结,已将此文整理成PDF文档了,需要的见文后下载获取方式. 全栈知识体系总览 Java入门与进阶面向对象与Java基础 Java 基础 - 面向对象 Java 基础 - 知识点 ...
- 视频教程-Java进阶高手课-Spring精讲精练-Java
[ [这里是图片001] Java进阶高手课-Spring精讲精练 中国科学技术大学硕士研究生,丹麦奥尔堡大学访问学者,先后就职于eBay.蚂蚁金服.SAP等国内外一线互联网公司,在Java后端开发. ...
- Java架构师知识体系汇总
Java架构师知识体系汇总 源码分析 常用设计模式 Proxy代理模式 Factory工厂模式 Singleton单例模式 Delegate委派模式 Strategy策略模式 Prototype原型模 ...
- 1. JAVA全栈知识体系--- Java基础
1. JAVA全栈知识体系- Java基础 文章目录 1. JAVA全栈知识体系--- Java基础 1.1 语法基础 面向对象特性? a = a + b 与 a += b 的区别 3*0.1 == ...
最新文章
- ATG中的定时Job处理
- 使用ORM Profiler分析数据访问性能
- 根据excel定义的表机构,导入powerdesigner
- (56)zabbix Screens视图配置
- luogu 4768
- 【转载】会议是浪费工作时间的最佳去处
- 源码分析Dubbo服务消费端启动流程
- eclipse maven项目 class类部署不到tomcat下_Springboot介绍以及用Eclipse搭建一个简单的Springboot项目教程
- 自学前端一般几年可以精通,找个差不多的工作?
- [转]服务器监控 UptimeRobot 简明使用手册
- 神州数码c语言笔试题,神州数码应聘笔试题(2)
- WPF 自定义文本框输入法 IME 跟随光标
- 拼多多和酷家乐面试经历总结(已拿offer)
- 阿里巴巴大数据技术专家岗面试题
- ubuntu 22.04 网易云音乐安装
- 基于python的图像识别
- vue单页面应用项目优化总结
- Rockchip RK3588 kernel dts解析之显示模块
- Easyrecovery教你Excel表格数据恢复
- ElasticSearch-查询语法(结构化查询)