1 What is a deadlock
2 deadlock impact
Influence deadlock in different systems is not the same, depending on the system's ability to handle deadlock
- Database : Detects and abandon the transaction
- JVM : Unable to automatically process
3 probability is not high but the big harm
- Not necessarily happen, but to comply with " Murphy's Law "
- In the event, mostly high concurrency scenarios, multi-user impact
- The whole system crashes , crashes subsystem, performance degradation
- Stress tests can not identify all potential deadlocks
4 is an example of deadlock
4.1 In the simplest case
/**
* 必定发生死锁的情况
*/
public class MustDeadLock implements Runnable {
static final Object LOCK_A = new Object();
static final Object LOCK_B = new Object();
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"进入了run方法...");
if ("A".equals(Thread.currentThread().getName())) {
synchronized (LOCK_A) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_B) {
System.out.println("线程A成功拿到两把锁");
}
}
}
if ("B".equals(Thread.currentThread().getName())) {
synchronized (LOCK_B) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_A) {
System.out.println("线程B成功拿到两把锁");
}
}
}
}
public static void main(String[] args) {
MustDeadLock run = new MustDeadLock();
Thread t1 = new Thread(run, "A");
Thread t2 = new Thread(run, "B");
t1.start();
t2.start();
}
}
Examples of actual production of 4.2: Transfer
/**
* 转账时候遇到死锁,一旦打开注释,便会发生死锁
*/
public class TransferMoney {
//A的账户余额
static Account a = new Account(500);
//B的账户余额
static Account b = new Account(500);
/**
* 转账业务
* @param from 钱从哪里来
* @param to 钱到哪里去
* @param amount 转账金额
*/
public static void transferMoney(Account from, Account to, int amount) {
//注意这里的加锁对象是谁
synchronized (from) {
//try {
// Thread.sleep(500);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足,转账失败。");
}
from.balance -= amount;
to.balance += amount;
System.out.println(Thread.currentThread().getName() + "成功转账" + amount + "元");
}
}
}
//账号类
static class Account {
//余额
int balance;
public Account(int balance) {
this.balance = balance;
}
}
public static void main(String[] args) throws InterruptedException {
Runnable run = () -> {
//A->B
if ("A".equals(Thread.currentThread().getName())) {
transferMoney(a, b, 200);
}
//B->A
if ("B".equals(Thread.currentThread().getName())) {
transferMoney(b, a, 300);
}
};
Thread t1 = new Thread(run, "A");
Thread t2 = new Thread(run, "B");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
}
4.3 analog multiplayer random transfer
50,000 people a lot, but still a deadlock occurs, Murphy's Law
Review: deadlock probability is not high , but great harm
public class TransferMoney {
/**
* 转账业务
* @param from 钱从哪里来
* @param to 钱到哪里去
* @param amount 转账金额
*/
public static void transferMoney(Account from, Account to, int amount) {
//注意这里的加锁对象是谁
synchronized (from) {
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足,转账失败。");
}
from.balance -= amount;
to.balance += amount;
System.out.println(Thread.currentThread().getName() + "成功转账" + amount + "元");
}
}
}
//账号类
static class Account {
//余额
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
/**
* 多人同时转账,依然很危险
*/
public class MultiTransferMoney {
public static void main(String[] args) {
//账户数量(数量越大发生死锁机率越小)
int numAccounts = 5000;
//每个账户的初始金额
int numMoney = 1000;
//转账次数(次数越小发生死锁机率越小)
int numIterations = 1000000;
//多少个人模拟转账
int numThreads = 20;
Random rnd = new Random();
Account[] accounts = new Account[numAccounts];
//初始化账户
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new Account(numMoney);
}
Runnable run = () -> {
//我转给你,你转给我的概率很小
for (int i = 0; i < numIterations; i++) {
int fromAcct = rnd.nextInt(numAccounts);
int toAcct = rnd.nextInt(numAccounts);
int amount = rnd.nextInt(numMoney);
TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
}
System.out.println("运行结束");
};
//模拟numThreads个人同时转账
for (int i = 0; i < numThreads; i++) {
new Thread(run).start();
}
//形成环了,卡死
}
}
4 5 deadlock prerequisite
Indispensable, while meeting
- Mutually exclusive conditions
- Request to maintain the conditions
- Not deprivation
- Circular wait condition
6 how to locate a deadlock?
6.1 jstack
Examples of practical production execution 4.2: Transfer code
1. Locate the java program corresponding to the process pid jvisualvm
2. execution jstack command jstack 40732
4.3 Analysis of simulation multiplayer random transfer
6.2 ThreadMXBean
/**
* 用ThreadMXBean检测死锁
*/
public class ThreadMXBeanDetection implements Runnable {
static final Object LOCK_A = new Object();
static final Object LOCK_B = new Object();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "进入了run方法...");
if ("A".equals(Thread.currentThread().getName())) {
synchronized (LOCK_A) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_B) {
System.out.println("线程A成功拿到两把锁");
}
}
}
if ("B".equals(Thread.currentThread().getName())) {
synchronized (LOCK_B) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK_A) {
System.out.println("线程B成功拿到两把锁");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadMXBeanDetection run = new ThreadMXBeanDetection();
Thread t1 = new Thread(run, "A");
//System.out.println("线程A,id=" + t1.getId());
Thread t2 = new Thread(run, "B");
//System.out.println("线程B,id=" + t2.getId());
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() + " id:" + deadlockedThreads[i]);
}
}
}
}
7 修复死锁的策略
线上问题都需要防患于未然,不造成损失地扑灭几乎已经是不可能
保存案发现场然后立刻重启服务器
暂时保证线上服务的安全,然后在利用刚才保存的信息,排查死锁,修改代码,重新发版
7.1 避免策略
避免策略:哲学家就餐的换手方案、转账换序方案
思路:避免相反的获取锁的顺序
- 转账时避免死锁
- 实际上不在乎获取锁的顺序
- 通过hashcode来决定获取锁的顺序、冲突时需要 “加时赛”
有主键就更方便
public class TransferMoney {
//A的账户余额
static Account a = new Account(500);
//B的账户余额
static Account b = new Account(500);
static final Object LOCK = new Object();
/**
* 转账业务
* @param from 钱从哪里来
* @param to 钱到哪里去
* @param amount 转账金额
*/
public static void transferMoney(Account from, Account to, int amount) {
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println(Thread.currentThread().getName() + "余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance += amount;
System.out.println(Thread.currentThread().getName() + "成功转账" + amount + "元");
}
}
//获取锁的顺序一致
//AB转账,A的hash值小于B
//A->B 先获取A的锁,在获取B的锁
//B->A 先获取A的锁,在获取B的锁
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();
}
}
}
//通过hashcode来决定获取锁的顺序、冲突时需要 “加时赛”
else {
synchronized (LOCK) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}
}
}
//账号类
static class Account {
//余额
int balance;
public Account(int balance) {
this.balance = balance;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
//A->B
transferMoney(a, b, 200);
}, "A");
Thread t2 = new Thread(() -> {
//B->A
transferMoney(b, a, 300);
}, "B");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
}
代码演示:哲学家进入死锁
多种解决方案
有死锁和资源耗尽的风险
- 服务员检查(避免策略)
- 改变—个哲学家拿叉子的顺序(避免策略)
- 餐票(避免策略)
- 领导调节(检测与恢复策略)
/**
* 演示哲学家就餐问题导致的死锁
*/
public class DiningPhilosophers {
//哲学家
public static class Philosopher implements Runnable {
//左边筷子
private Object leftChopstick;
//右边筷子
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public 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);
//[0,10)
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];
//1.发生死锁 都是Picked up left chopstick
//philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
//2.改变—个哲学家拿叉子的顺序(避免策略)
//让最后一个哲学家,反着拿
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();
}
}
}
7.2 检测与恢复策略
检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
7.3 鸵鸟策略
鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。
8 实际工程中如何避免死锁
代码演示:退一步海阔天空
/**
* 用tryLock来避免死锁
*/
public class TryLockDeadlock implements Runnable {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
@Override
public void run() {
//尝试获取次数
for (int i = 0; i < 100; i++) {
if ("A".equals(Thread.currentThread().getName())) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程A获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程A获取到了锁2");
System.out.println("线程A成功获取到了两把锁");
//do something
lock2.unlock();
lock1.unlock();
break;
} else {
System.out.println("线程A尝试获取锁2失败,已重试");
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程A获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if ("B".equals(Thread.currentThread().getName())) {
try {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程B获取到了锁2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程B获取到了锁1");
System.out.println("线程B成功获取到了两把锁");
//do something
lock1.unlock();
lock2.unlock();
break;
} else {
System.out.println("线程B尝试获取锁1失败,已重试");
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程B获取锁2失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TryLockDeadlock run = new TryLockDeadlock();
new Thread(run,"A").start();
new Thread(run,"B").start();
}
}
9 其他活性故障
- 死锁是最常见的活跃性问题,不过除了死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题
- 活锁(LiveLock)
- 饥饿
9.1 活锁
9.1.2 什么是活锁
9.1.3 代码演示
/**
* 演示活锁问题
*/
public class LiveLock {
//餐具:勺子(只有一个)
static class Spoon {
//勺子拥有者
private Diner owner;
public Spoon(Diner owner) {
this.owner = owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
//使用勺子
public synchronized void use() {
System.out.printf("%s吃完了!", owner.name);
}
}
//就餐者:正在吃饭的那个人
static class Diner {
//就餐者名字
private String name;
//就餐者是否饥饿
private boolean isHungry;
public Diner(String name) {
this.name = name;
//初始化饥饿
isHungry = true;
}
/**
* 就餐方法
* @param spoon 餐具(物资匮乏,只有一个)
* @param spouse 陪我一起就餐的人
*/
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) {
//原因:重试机制不变,消息队列始终重试,吃饭始终谦让
//解决:加入随机因素(以太网的指数退避算法)
if (spouse.isHungry && new 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);
}
}
}
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();
}
}
9.1.4 工程中的活锁实例:消息队列
9.1.5 如何解决活锁问题
加入随机因素
9.2 饥饿
当线程需要某些资源(例如CPU),但是却始终得不到
线程的优先级设置得过于低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁
饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差
10 常见面试问题
1. 写一个必然死锁的例子,生产中什么场景下会发生死锁?
- 在一个方法中获取多个锁,容易产生死锁
2. 发生死锁必须满足哪些条件?
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等待条件
3. 如何定位死锁?
- jstack
- ThreadMXBean
4. 有哪些解决死锁问题的策略?
- 避免策略:哲学家就餐的换手方案、转账换序方案
- 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
- 鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。
5.讲一讲经典的哲学家就餐问题
- 画出示意图
- 给出多种解决方案
- 服务员检查(避免策略)
- 改变一个哲学家拿叉子的顺序(避免策略)
- 餐票(避免策略)
- 领导调节(检测与恢复策略)
6. 实际工程中如何避免死锁?
- 设置超时时间
- 多使用并发类而不是自己设计锁
- 尽量降低锁的使用粒度:用不同的锁而不是一个锁
- 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
- 给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践
- 避免锁的嵌套:MustDeadLock类
- 分配资源前先看能不能收回来:银行家算法
- 尽量不要几个功能用同一把锁:专锁专用
7. 什么是活跃性问题?活锁、饥饿和死锁有什么区别?