Java并发编程(四)--- 死锁的发生与避免

举报
码农飞哥 发表于 2021/05/29 14:09:59 2021/05/29
【摘要】 文章首发于:Java并发编程(四)— 死锁的发生与避免 前言 上一篇我们介绍了如何通过synchronized 来加锁保护资源。但是,不当的加锁方式可能就会导致死锁。 死锁发生的场景 最典型的就是哲学家问题, 场景:5个哲学家,5跟筷子,5盘意大利面,大家围绕桌子而坐,进行思考与进食活动。 哲学家的活动描述: 哲学家除了吃面、还要思考、所以要么放下左右手筷子进...

文章首发于:Java并发编程(四)— 死锁的发生与避免

前言

上一篇我们介绍了如何通过synchronized 来加锁保护资源。但是,不当的加锁方式可能就会导致死锁。

死锁发生的场景

最典型的就是哲学家问题,
场景:5个哲学家,5跟筷子,5盘意大利面,大家围绕桌子而坐,进行思考与进食活动。
在这里插入图片描述
哲学家的活动描述:
哲学家除了吃面、还要思考、所以要么放下左右手筷子进行思考、要么拿起两个筷子(自己两侧的)开始吃面。
哲学家从不交谈,这就很危险了,很可能会发生死锁,假设每个人都是先拿到左边的筷子,然后去拿右边的筷子,那么就可能会出现如下情况。
在这里插入图片描述
通过代码模拟:

public class DeadLockTest2 { public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); int sum = 5; Chopsticks[] chopsticks = new Chopsticks[sum]; for (int i = 0; i < sum; i++) { chopsticks[i] = new Chopsticks(); } for (int i = 0; i < sum; i++) { executorService.execute(new Philosopher(chopsticks[i], chopsticks[(i + 1) % sum])); } }
	// 筷子 static class Chopsticks { }
	//哲学家 static class Philosopher implements Runnable { private Chopsticks left; private Chopsticks right; public Philosopher(Chopsticks left, Chopsticks right) { this.left = left; this.right = right; } @Override public void run() { try { //思考一段时间 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (left) { try { //拿到左边的筷子之后等待一段时间 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (right) { try { System.out.println("********开始吃饭"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
}

  
 
  • 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

如上程序:定义了一个哲学家类,该类的主要任务要么是思考,要么是吃饭,吃饭的话,首先拿到其左边的筷子,等待一段时间后再去拿其右边的筷子。在此处因为每个哲学家都是占用自己左边的筷子等待拿右边的筷子。所以,就会出现循环等待,导致死锁。下面我们就来查看下:

如何查看死锁的发生

我们可以通过java命令很方便的查看是否有死锁发生。首先通过jps命令查看当前程序所占的进程如下:
在这里插入图片描述
找到对应的进程之后,接着通过jstack 命令查看程序运行情况。如下:
在这里插入图片描述
通过上述分析我们发现死锁发生的条件是如下四个(必须同时满足):

  1. 互斥,共享资源A和B只能被一个线程占用,就是本例中的,一根筷子同一时刻只能被一个哲学家获得
  2. 占有且等待:线程T1持有共享资源A,在等待共享资源B时,不释放占用的资源,在本例中就是:哲学家1获得他左边的筷子,等待获得他右边的筷子,即使没有得到也不会放回其获得的筷子。
  3. 不可抢占:其他线程不能强行占用线程T1占用的资源,在本例中就是:每个哲学家获得的筷子不能被其他哲学家抢走。
  4. 循环等待:线程T1等待线程T2占用的资源,线程T2等待线程T1占用的资源。在本例中:所有哲学家围坐一桌,已经形成了一个申请资源的环。

如何避免死锁

前面我们说了,死锁的发生条件是必须同时满足上述四个条件。那么避免死锁的方式就是破坏掉其中的一个条件就可以了。

对于占用且等待

对于占用且等待的情况,我们只需要一次性申请所有的资源,只有申请到了才会往下面走。对于这种情况,我们需要一个调度者,由它来统一申请资源。调度者必须是单例的,由他给哲学家分配筷子。

public class Allocator { private List<Object> applyList = new ArrayList<Object>(); private final static Allocator allocator = new Allocator(); private Allocator() { } /** * 只能由一个人完成,所以是单例模式 * @return */ public static Allocator getAllocator() { return allocator; } /** * 申请资源 */ synchronized boolean applyResource(Object from, Object to) { if (applyList.contains(from) || applyList.contains(to)) { return false; } applyList.add(from); applyList.add(to); return true; } /** * 释放资源 */ synchronized void free(Object from, Object to) { applyList.remove(from); applyList.remove(to); }


  
 
  • 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

调度者会一直申请拿到两个资源,如果能拿到这执行后续流程,拿不到的话则一直循环申请。

public void eat(Account2 target) { //没有申请到锁就一直循环下去,直到成功 while (!Allocator.getAllocator().applyResource(this, target)) { return; } try { //左边 synchronized (this) { //右边 synchronized (target) { } } } finally { //释放已经申请的资源 Allocator.getAllocator().free(this, target); } }


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

对于不可抢占资源

对于不可抢占资源,占有部分资源的线程进一步申请其他资源,如果申请不到则主动释放它占用的资源。在后面我们会运用lock来实现。给锁设定超时时间。如果在超时未获得需要的资源,则释放其所占资源。

class Philosopher extends Thread{ private ReentrantLock left,right; public Philosopher(ReentrantLock left, ReentrantLock right) { super(); this.left = left; this.right = right; } public void run(){ try { while(true){ Thread.sleep(1000);//思考一段时间 left.lock(); try{ if(right.tryLock(1000,TimeUnit.MILLISECONDS)){ try{ Thread.sleep(1000);//进餐一段时间 }finally { right.unlock(); } }else{ //没有获取到右手的筷子,放弃并继续思考 } }finally { left.unlock(); } } } catch (InterruptedException e) { } }
}

  
 
  • 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

如上程序,我们将right锁的超时时间设置为1秒,如果不能获取到,右手的筷子,则放弃吃面并继续思考。

对于循环等待

对于循环等待,我们可以按序申请资源来预防,所谓的按序申请,是指资源是有线性顺序的,申请的时候可以先申请序号小的,再申请序号大的。
我们在Chopsticks中添加一个id 字段,作为资源的序号。然后在申请资源时按照序号从小到大开始申请。

   static class Chopsticks { private int id; public Chopsticks(int id) { this.id = id; } public int getId() { return id; } } public Philosopher(Chopsticks left, Chopsticks right) { if (left.getId() > right.getId()) { this.left = right; this.right = left; } else { this.left = left; this.right = right; } }

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

死锁发生后如何处理

当检测到死锁时,一个可行的做法是释放所有锁,回退,并且等待一段随机时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退 。而不会是因为加锁的请求超时了,虽然有回退和等待,但是如果有大量线程竞争同一批锁,它们还是会重复地死锁。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。可以再死锁发生的时候设置随机的优先级。

总结

本文通过一个经典的哲学家就餐的问题,引入了死锁发生的场景及发生的条件。然后,针对这些条件介绍了避免死锁的三种方式。

文章来源: feige.blog.csdn.net,作者:码农飞哥,版权归原作者所有,如需转载,请联系作者。

原文链接:feige.blog.csdn.net/article/details/102844796

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。