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写一个必然死锁
的例子,生产中什么场景下会发生死锁?
一个方法中获取多个锁。