kim.zhang

风在前,无惧!


  • 首页

  • 标签42

  • 分类12

  • 归档94

  • 搜索

死锁.md

发表于 2020-08-10 更新于 2021-11-21 分类于 并发
本文字数: 7.8k 阅读时长 ≈ 7 分钟

什么是死锁

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

多个线程造成死锁的情况:等待的资源,形成一个环

死锁的影响

  • 死锁的影响在不同的系统中是不一样的,这取决于系统对死锁的处理能力
    • 数据库中:检测并放弃事务
    • JVM中:无法自行处理,但是JVM能检测出死锁

一个必然造成死锁的栗子

  • 当类的对象flag=1时(T1),先锁定O1,睡眠500毫秒,然后锁定O2;
  • 而T1在睡眠的时候另一个flag=0的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放O1;
  • T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定;
  • T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定;
  • T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁。

同时注意一点,死锁的程序退出状态码是130.

Process finished with exit code 130

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* @Author Ming
* @Date 2020/07/22 22:07
* @Description 演示必然造成死锁的栗子,线程1先获取lock1锁,线程2先获取lock2锁
* 短暂休眠保证两个线程都获取了锁之后,之后争夺类锁会造成死锁
*/
public class MustHappenDeedLock implements Runnable {
// 控制不同的进程执行不同的代码块
private int flag;
// 必须加static关键字,如果是对象锁,则不会造成死锁,因为使用不同的对象创建线程
private static Object lock1 = new Object();
private static Object lock2 = new Object();

public static void main(String[] args) {
// 创建两个线程争夺类锁
MustHappenDeedLock mustHappenDeedLock1 = new MustHappenDeedLock();
MustHappenDeedLock mustHappenDeedLock2 = new MustHappenDeedLock();
mustHappenDeedLock1.flag = 0;
mustHappenDeedLock2.flag = 1;
new Thread(mustHappenDeedLock1).start();
new Thread(mustHappenDeedLock2).start();
}

@Override
public void run() {
if (flag == 0) {
synchronized (lock1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + "获得了两把锁");
}
}
}

if (flag == 1) {
synchronized (lock2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + "获得了两把锁");
}
}
}
}
}

死锁的4个必要条件

  • 互坼条件:在同一时刻,只能有一个线程拥有资源
  • 请求与保持条件:自己不释放资源,又去申请别的资源
  • 不剥夺条件:没有外界的干扰
  • 循环等待条件:多个线程没有形成环路

死锁的形成,这4个条件缺一不可

如何定位死锁

  • jstack(jvm命令,可以先用jps lvm查看JVM进程)
  • ThreadMXBean(代码)

修复死锁的策略

线上发生死锁应该怎么办

  • 保存案发现场,然后立刻重启服务器
  • 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发布

1.避免策略

  • 避免相反的获取锁的顺序
    * 通过hashcode来决定获取锁的顺序,冲突时需要“加时赛”
      * 实际工程中可以利用数据库的主键
  • 哲学家就餐问题

2.检测与恢复策略

​ 一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁

image-20200723212820862

3.鸵鸟策略

​ 如果发生死锁的概率非常低,我们就直接忽略它,直到死锁发生的时候,再人工修复。

栗子1:哲学家就餐问题(换序避免死锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* @Author Ming
* @Date 2020/07/21 20:20
* @Description 哲学家就餐问题演示
*/
public class DiningPhilosopher {
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);
}
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲学家" + (i + 1)).start();
}
}

// 一个线程代表一个哲学家
public static class Philosopher implements Runnable {
// 锁对象代表筷子
private Object left = new Object();
private Object right = new Object();

public Philosopher(Object left, Object right) {
this.left = left;
this.right = right;
}


@Override
public void run() {
while (true) {
doAction("thinking");
// 获取左边的筷子
synchronized (left) {
doAction("获取左边的筷子");
synchronized (right) {
doAction("获取右边的筷子- eating");
}
doAction("放下右边的筷子");
}
doAction("放下左边的筷子");

}
}

private void doAction(String action) {
System.out.println(Thread.currentThread().getName() + ":" + action);
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

另:哲学界就餐问题解决的4种方案:

  • 服务员检查(避免策略)
  • 改变一个哲学界拿叉子的顺序,换序(避免策略)
  • 餐票,只有拿到餐票的才能就餐,就餐完返回餐票(避免策略)
  • 领导调节(检测与恢复策略)

栗子2:银行转账(hashCode换序避免死锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* @Author Ming
* @Date 2020/07/22 22:28
* @Description 银行转账的栗子,演示死锁的发生,以及通过换序避免死锁的产生
*/
public class TransferMoneyDeedLock implements Runnable {
private static Account account1 = new Account(500);
private static Account account2 = new Account(500);
private int flag;
private static Object lock = new Object();

public static void main(String[] args) throws InterruptedException {
TransferMoneyDeedLock transferMoneyDeedLock1 = new TransferMoneyDeedLock();
TransferMoneyDeedLock transferMoneyDeedLock2 = new TransferMoneyDeedLock();
transferMoneyDeedLock1.flag = 0;
transferMoneyDeedLock2.flag = 1;
Thread thread1 = new Thread(transferMoneyDeedLock1, "account1");
Thread thread2 = new Thread(transferMoneyDeedLock2, "account2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("account1 balance:" + account1.balance);
System.out.println("account2 balance:" + account2.balance);
}

@Override
public void run() {
if (flag == 0) {
transferMoney(account1, account2, 200);
}
if (flag == 1) {
transferMoney(account2, account1, 200);
}
}

public static void transferMoney(Account from, Account to, Integer amount) {
// 获取对象的hashcode来决定获取锁的顺序
int fromHashCode = System.identityHashCode(from);
int toHashCode = System.identityHashCode(to);
if (fromHashCode > toHashCode) {
synchronized (from) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to) {
transferHelper(from, to, amount);
}
}
} else if (fromHashCode < toHashCode) {
synchronized (to) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (from) {
transferHelper(from, to, amount);
}
}
} else {
// hash冲突的情况,额外加一把锁,公平竞争执行的机会
synchronized (lock) {
// 先对from加锁还是to加锁已经不重要了,有了外层的lock,from和to不会同时被获取
synchronized (to) {
synchronized (from) {
transferHelper(from, to, amount);
}
}
}
}
}

public static void transferHelper(Account from, Account to, Integer amount) {
if (from.balance - amount > 0) {
from.balance -= amount;
to.balance += amount;
System.out.println(Thread.currentThread().getName() + "转账" + amount);
} else {
System.out.println("转账失败");
}
}

static class Account {
private Integer balance;

public Account(Integer balance) {
this.balance = balance;
}
}
}

实际工程中如何避免死锁

  • 设置超时时间
  • 多使用并发类而不是自己设计锁
  • 尽量降低锁的使用粒度:用不同的锁而不是一个锁
  • 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
  • 给线程起个有意义的名字:debug和排查时事半功倍
  • 避免锁的嵌套
  • 分配资源前先看能不能收回来:银行家算法
  • 尽量不要几个功能用同一把锁:专锁专用

什么是活锁

虽然线程并没有阻塞,也始终在运行,但是程序却得不到进展,因为线程始终重复做同样的事

栗子2:牛郎织女没饭吃(加入随机因素解决活锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @Author Ming
* @Date 2020/07/21 21:47
* @Description 演示活锁,牛郎和织女互相谦让勺子,最后谁都吃不上饭
*/
public class LiveLock {
public static void main(String[] args) {
Diner boy = new Diner("牛郎");
Diner girl = new Diner("织女");
// 牛郎先拥有勺子
Spoon spoon = new Spoon(boy);

new Thread(() -> boy.eatWith(spoon, girl)).start();
new Thread(() -> girl.eatWith(spoon, boy)).start();
}

static class Spoon {
private Diner owner;

public Spoon(Diner owner) {
this.owner = owner;
}

public synchronized void use() {
System.out.println(owner.name + "正在使用勺子");
}
}

static class Diner {
private String name;
private boolean isHunger;

public Diner(String name) {
this.name = name;
isHunger = true;
}

public void eatWith(Spoon spoon, Diner partner) {
while (this.isHunger) {
// 先判断勺子在不在自己手上
if (this != spoon.owner) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
// 判断女朋友饿不饿
// 加入随机因素解决活锁
int random = ThreadLocalRandom.current().nextInt(10);
if (partner.isHunger && random < 9) {
System.out.println(this.name + ":亲爱的,你先吃吧");
spoon.owner = partner;
continue;
}

// 勺子在自己手上
spoon.use();
this.isHunger = false;
System.out.println(this.name + "吃完了");
spoon.owner = partner;
}
}
}
}

什么是饥饿

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

  • 线程的优先级设置得过于低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁
  • 饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差
一毛也是爱~
Kim.Zhang 微信支付

微信支付

# 并发基础
JOIN方法.md
CyclicBarrier.md
  • 文章目录
  • 站点概览
Kim.Zhang

Kim.Zhang

且行且珍惜
94 日志
12 分类
42 标签
E-Mail Weibo
  1. 1. 什么是死锁
  2. 2. 死锁的影响
  3. 3. 一个必然造成死锁的栗子
  4. 4. 死锁的4个必要条件
  5. 5. 如何定位死锁
  6. 6. 修复死锁的策略
  7. 7. 栗子1:哲学家就餐问题(换序避免死锁)
  8. 8. 栗子2:银行转账(hashCode换序避免死锁)
  9. 9. 实际工程中如何避免死锁
  10. 10. 什么是活锁
  11. 11. 栗子2:牛郎织女没饭吃(加入随机因素解决活锁)
  12. 12. 什么是饥饿
粵ICP备19091267号 © 2019 – 2022 Kim.Zhang | 629k | 9:32
本站总访问量 4 次 | 有 309 人看我的博客啦 |
博客全站共176.7k字
载入天数...载入时分秒...
0%