1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > 【多线程】并发死锁问题与企业级解决方案

【多线程】并发死锁问题与企业级解决方案

时间:2021-12-28 02:16:09

相关推荐

【多线程】并发死锁问题与企业级解决方案

1. 死锁是什么?有什么危害

1.1什么是死锁

发生在并发

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

如果个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能发生死锁。

1.2 死锁的影响

死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力

数据库中:检测并放弃事务

JVM中:无法自动处理

1.3 几率不高但危害大

不一定发生,但是遵守墨菲定律

西方的“墨菲定律”是这样说的:Anything that can go wrong will go wrong:“凡事只要有可能出错,那就一定会出错。”

一旦发生,多是高并发场景,影响用户多

整个系统崩溃、子系统崩溃、性能降低

压力测试无法找出所有潜在的死锁

2. 发生死锁的例子

2.1 最简单的情况

2.1.1 代码

/*** 描述:必定发生死锁的情况* */public class MustDeadLock implements Runnable{int flag = 1;static Object o1 = new Object();static Object o2 = new Object();@Overridepublic void run() {System.out.println("flag=" + flag);if(flag == 1){synchronized (o1){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){System.out.println("线程1获取到了o2的锁");}}}if(flag == 2){synchronized (o2){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){System.out.println("线程2获取到o1的锁");}}}}public static void main(String[] args) {MustDeadLock r1 = new MustDeadLock();MustDeadLock r2 = new MustDeadLock();r1.flag = 1;r2.flag = 2;new Thread(r1).start();new Thread(r2).start();}}

2.1.2 分析

假设线程r1先于线程r2执行,获取到o1的锁,开始执行sleep;

此时线程r2开始执行,获取到o2的锁,开始执行sleep;

线程r1执行完sleep准备获取o2的锁,但是由于r2还在sleep,无法获取o2的锁。

线程r2执行完sleep准备获取o1的锁,但是由于线程r1在阻塞,因此没法获取o1的锁。

线程r1和线程r2都持有对方需要的资源进入阻塞状态,死锁产生。

2.1.3 注意查看退出信号

Process finished with exit code 130

是不正常的退出信号,对比正常结束的程序信号是0

2.2 实际生产中的例子:转账

2.2.1 代码

public class TransferMoney implements Runnable{private int flag = 0;static BankAccount a = new BankAccount(500);static BankAccount b = new BankAccount(500);@Overridepublic void run() {if(flag == 0){transferMoney(a,b, 200);}if(flag == 1){transferMoney(b, a, 200);}}public static void transferMoney(BankAccount from, BankAccount to,int amount){synchronized (from){//TODO 在此处添加代码时会导致代码死锁synchronized (to){if(from.balance - amount <0){System.out.println("余额不足,不可转账!");}from.balance -= amount;to.balance += amount;System.out.println("转完钱了");}}}public static void main(String[] args) throws InterruptedException {TransferMoney r1 = new TransferMoney();TransferMoney r2 = new TransferMoney();r1.flag = 0;r2.flag = 1;Thread thread1 = new Thread(r1);Thread thread2 = new Thread(r2);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("a.balance="+ a.balance +",b.balance=" + b.balance);}static class BankAccount{private int balance;public BankAccount(int balance) {this.balance = balance;}}}

2.3 模拟多人随机转账

/*** 多人同时转账,依旧很危险* */public class MultiTransferMoney{private static final int USER_COUNT = 1000;//用户总数private static final int TRADES_NUM = 20000;//交易次数private static final int ACCOUNT_MONEY = 1000;private static final int NUM_THREADS = 100;public static void main(String[] args) {Random random = new Random();TransferMoney.BankAccount [] accounts = new TransferMoney.BankAccount[USER_COUNT];for (int i = 0; i < USER_COUNT; i++) {accounts[i] = new TransferMoney.BankAccount(ACCOUNT_MONEY);}class TransferThread extends Thread{@Overridepublic void run() {for (int i = 0; i < TRADES_NUM; i++) {int fromAccount = random.nextInt(USER_COUNT);int toAccount = random.nextInt(USER_COUNT);int money = random.nextInt(ACCOUNT_MONEY);TransferMoney.transferMoney(accounts[fromAccount],accounts[toAccount],money);}}}for (int i = 0; i < NUM_THREADS; i++) {System.out.println("========================" +i);new TransferThread().start();}}}

3. 死锁的4个必要条件

3.1 互斥条件

同一个资源每次只能被一个线程(进程)使用。

3.2 请求与保持条件

第一个线程去请求第二把锁,但是保留第一把锁。

3.3 不剥夺条件

3.4 循环等待条件

构成环路

缺一不可,逐个分析之前的例子

4. 如何定位死锁?

jstack

jps指令可以获取到当前运行Java程序的PID

执行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 o1 = new Object();static Object o2 = new Object();@Overridepublic void run() {System.out.println("flag=" + flag);if(flag == 1){synchronized (o1){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2){System.out.println("线程1获取到了o2的锁");}}}if(flag == 2){synchronized (o2){try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1){System.out.println("线程2获取到o1的锁");}}}}public static void main(String[] args) throws InterruptedException {ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();r1.flag = 1;r2.flag = 2;new Thread(r1).start();new Thread(r2).start();TimeUnit.SECONDS.sleep(1);ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();for (int i = 0; i < deadlockedThreads.length; i++) {ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);System.out.println("dead locked thread name is : " + threadInfo.getThreadName());}}}

运行结果

flag=2flag=1dead locked thread name is : Thread-1dead locked thread name is : Thread-0

5. 修复死锁的策略

保存案发现场然后立刻重启服务器(使用Java相关的命令把整个堆栈信息保存下来)

暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码重新发版。

线上发生死锁应该怎么办?

常见修复策略

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

思路:避免相反的获取锁的顺序

public class TransferMoney implements Runnable{private int flag = 0;static BankAccount a = new BankAccount(500);static BankAccount b = new BankAccount(500);@Overridepublic void run() {if(flag == 0){transferMoney(a,b, 200);}if(flag == 1){transferMoney(b, a, 200);}}public static void transferMoney(BankAccount from, BankAccount 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 fromObjHash = System.identityHashCode(from);int toObjHash = System.identityHashCode(to);if(fromObjHash > toObjHash){synchronized (from){//TODO 在此处添加代码时会导致代码死锁synchronized (to){new Helper().transfer();}}}else if(fromObjHash < toObjHash){synchronized (to){//TODO 在此处添加代码时会导致代码死锁synchronized (from){new Helper().transfer();}}}}public static void main(String[] args) throws InterruptedException {TransferMoney r1 = new TransferMoney();TransferMoney r2 = new TransferMoney();r1.flag = 0;r2.flag = 1;Thread thread1 = new Thread(r1);Thread thread2 = new Thread(r2);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("a.balance="+ a.balance +",b.balance=" + b.balance);}static class BankAccount{private int balance;public BankAccount(int balance) {this.balance = balance;}}}

通过hashcode来决定获取锁的顺序、冲突(哈希碰撞)时需要加赛

public class TransferMoney implements Runnable{private int flag = 0;static BankAccount a = new BankAccount(500);static BankAccount b = new BankAccount(500);static Object lock = new Object();@Overridepublic void run() {if(flag == 0){transferMoney(a,b, 200);}if(flag == 1){transferMoney(b, a, 200);}}public static void transferMoney(BankAccount from, BankAccount 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 fromObjHash = System.identityHashCode(from);int toObjHash = System.identityHashCode(to);if(fromObjHash > toObjHash){synchronized (from){//TODO 在此处添加代码时会导致代码死锁synchronized (to){new Helper().transfer();}}}else if(fromObjHash < toObjHash){synchronized (to){//TODO 在此处添加代码时会导致代码死锁synchronized (from){new Helper().transfer();}}}else {synchronized (from){synchronized (to){new Helper().transfer();}}}}public static void main(String[] args) throws InterruptedException {TransferMoney r1 = new TransferMoney();TransferMoney r2 = new TransferMoney();r1.flag = 0;r2.flag = 1;Thread thread1 = new Thread(r1);Thread thread2 = new Thread(r2);thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("a.balance="+ a.balance +",b.balance=" + b.balance);}static class BankAccount{private int balance;public BankAccount(int balance) {this.balance = balance;}}}

演示哲学家就餐问题导致的死锁

先拿起左手的筷子然后拿起右手的筷子如果筷子被别人使用,那就等别人用完。领导调节(检测与恢复策略)

/*** 演示哲学家就餐问题导致的死锁* */public class DiningPhilosophers {static class Philosopher implements Runnable{static Random random = new Random();private Object leftChopstick;private Object rightChopstick;public Philosopher(Object leftChopstick, Object rightChopstick) {this.leftChopstick = leftChopstick;this.rightChopstick = rightChopstick;}@Overridepublic void run() {while (true){try {doThing("思考");synchronized (leftChopstick){doThing("拿起左手边的筷子");synchronized (rightChopstick){doThing("拿起右手边的筷子");doThing("放下右手边的筷子");}doThing("放下左手边的筷子");}} catch (InterruptedException e) {e.printStackTrace();}}}public void doThing(String things) throws InterruptedException {System.out.println("第"+Thread.currentThread().getName() +"号哲学家正在" + things);TimeUnit.SECONDS.sleep(random.nextInt(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 rightChopStick = chopsticks[i];Object leftChopStick = chopsticks[(i+1)%5];philosophers[i] = new Philosopher(leftChopStick,rightChopStick);new Thread(philosophers[i],""+(i+1)).start();}}}

哲学家就餐问题的多种解决方案

服务员检查(避免策略)改变一个哲学家拿叉子的顺序(避免策略)

import java.util.concurrent.TimeUnit;/*** 演示哲学家就餐问题导致的死锁* */public class DiningPhilosophers {static class Philosopher implements Runnable{private Object leftChopstick;private Object rightChopstick;public Philosopher(Object leftChopstick, Object rightChopstick) {this.leftChopstick = leftChopstick;this.rightChopstick = rightChopstick;}@Overridepublic void run() {while (true){try {doThing("思考");synchronized (leftChopstick){doThing("拿起左手边的筷子");synchronized (rightChopstick){doThing("拿起右手边的筷子");doThing("放下右手边的筷子");}doThing("放下左手边的筷子");}} catch (InterruptedException e) {e.printStackTrace();}}}public void doThing(String things) throws InterruptedException {System.out.println("第"+Thread.currentThread().getName() +"号哲学家正在" + things);TimeUnit.SECONDS.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 rightChopStick = chopsticks[i];Object leftChopStick = chopsticks[(i+1)%5];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();}}}

餐票(避免策略)

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

鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。

6. 实际工程中如何避免死锁?

6.1 设置超时时间

Lock的tryLock(long timeout, TimeUnit unit)

synchronized不具备尝试锁的能力

造成超时的可能性很多:发生了死锁、线程陷入死循环、线程执行很慢

获取锁失败:打日志、发报警邮件、重启

代码演示退一步海阔天空

/*** 用tryLock避免死锁* */public class TryLockDeadLock implements Runnable{private static Lock lock1 = new ReentrantLock();private static Lock lock2 = new ReentrantLock();private int flag =0;@Overridepublic void run() {while (true){if(flag == 0){try {if(lock1.tryLock(900, TimeUnit.MILLISECONDS)){System.out.println("lock1\t"+Thread.currentThread().getName() +"\t获取到了lock1");TimeUnit.SECONDS.sleep((long) (Math.random()*10));if(lock2.tryLock(900,TimeUnit.MILLISECONDS)){System.out.println("lock2\t"+Thread.currentThread().getName() +"\t获取到了lock2");TimeUnit.SECONDS.sleep((long) (Math.random()*10));lock2.unlock();lock1.unlock();System.out.println("lock2\t"+Thread.currentThread().getName() +"\t释放了lock2");System.out.println("lock1\t"+Thread.currentThread().getName() +"\t释放了lock1");break;}else {lock1.unlock();System.out.println("lock1\t"+Thread.currentThread().getName() +"\t释放了lock1");}}} catch (InterruptedException e) {e.printStackTrace();}}if(flag == 1){try {if(lock2.tryLock(1900, TimeUnit.MILLISECONDS)){TimeUnit.SECONDS.sleep((long) (Math.random()*10));System.out.println("lock2\t"+Thread.currentThread().getName() +"\t获取到了lock2");if(lock1.tryLock(1900,TimeUnit.MILLISECONDS)){TimeUnit.SECONDS.sleep((long) (Math.random()*10));System.out.println("lock1\t"+Thread.currentThread().getName() +"\t获取到了lock1");lock1.unlock();lock2.unlock();System.out.println("lock1\t"+Thread.currentThread().getName() +"\t释放了lock1");System.out.println("lock2\t"+Thread.currentThread().getName() +"\t释放了lock2");break;}else {lock2.unlock();System.out.println("lock2\t"+Thread.currentThread().getName() +"\t释放了lock2");}}} catch (InterruptedException e) {e.printStackTrace();}}}}public static void main(String[] args) {TryLockDeadLock r1 = new TryLockDeadLock();r1.flag =1;TryLockDeadLock r2 = new TryLockDeadLock();r2.flag =0;new Thread(r1,"线程1").start();new Thread(r2,"线程2").start();}}

程序输出结果:

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

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

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

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

6.5 给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践

6.6 避免使用锁嵌套

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

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

7. 其它活性故障(活跃性问题)

7.1 死锁

7.2 活锁

7.2.1什么是活锁

定义:虽然线程并没有阻塞,也始终在运行(所以叫"活"锁,线程是"活"的),但是程序却得不到进展,因为线程始终重复做同样的事。

和死锁很类似,会造成线程无法继续运行的情况,但是此时线程并没有阻塞,还在运行。

死锁:每个哲学家都会拿着左手的餐叉,永远都在等右边的餐叉(或者相反)

活锁:在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。

在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源

7.2.2 代码演示

/*** 演示活锁问题* */public class LiveLock {static class Spoon{//餐具private Diner dinerOwner;public Spoon(Diner dinerOwner) {this.dinerOwner = dinerOwner;}public synchronized void use(){System.out.printf("我是%s,我要干饭了\n",dinerOwner.name);}public Diner getDinerOwner() {return dinerOwner;}public void setDinerOwner(Diner dinerOwner) {this.dinerOwner = dinerOwner;}}static class Diner {//就餐者private String name;private boolean isHungry;public Diner(String name) {this.name = name;this.isHungry = true;}/*** 先检查勺子是否在自己手里,然后检查对方是否饥饿,如果对方不饿,自己才能吃* */private void eatWith(Spoon spoon, Diner other){while (this.isHungry){if(spoon.dinerOwner != this){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}continue;}if(other.isHungry){//检查对方是不是处于饥饿状态System.out.printf("我是%s,亲爱的%s,你先恰饭吧\n",name,other.name);spoon.setDinerOwner(other);continue;}//轮到自己吃了spoon.use();this.isHungry = false;System.out.printf("%s吃完了\n",this.name);spoon.setDinerOwner(other);}}public String getName() {return name;}public void setName(String name) {this.name = name;}public boolean isHungry() {return isHungry;}public void setHungry(boolean hungry) {isHungry = hungry;}}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.2.3 工程中的活锁实例:消息队列

策略:消息如果处理失败,就放在出列开头重试

由于依赖服务出了问题,处理该消息一直失败

没阻塞,但程序无法继续

解决放到队列尾部,重试限制

7.2.4 如何解决活锁问题

原因:重试机制不变,消息队列始终重试,吃饭始终谦让

以太网的指数退避算法

加入随机因素

代码演示

Random random = new Random();if(other.isHungry && random.nextInt(10)<9){//检查对方是不是处于饥饿状态System.out.printf("我是%s,亲爱的%s,你先恰饭吧\n",name,other.name);spoon.setDinerOwner(other);continue;}

7.3 饥饿

当线程需要某些资源(例如CPU),但是始终得不到

线程的优先级设置得过低,或者又某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁

饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作) , 另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差。

8. 面试常考问题

8.1写一个必然死锁的例子,生产中什么场景下会发生死锁?

一个方法中获取多个锁。

8.2发生死锁必须满足哪些条件?

8.3如何定位死锁?

8.4 有哪些解决死锁问题的策略?

8.5 讲一讲经典哲学家就餐问题

8.6 实际工程中如何避免死锁

8.7 什么是活跃性问题?活锁、饥饿、和死锁有什么区别?

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。