Java并发编程(十)死锁
哲学家进餐问题
并发执行带来的最棘手的问题莫过于死锁了,死锁问题中最经典的案例就是哲学家进餐问题:5个哲学家坐在一个桌子上,桌子上有5根筷子,每个哲学家的左手边和右手边各有一根筷子。示意图如下:
哲学家进餐问题
并发执行带来的最棘手的问题莫过于死锁了,死锁问题中最经典的案例就是哲学家进餐问题:5个哲学家坐在一个桌子上,桌子上有5根筷子,每个哲学家的左手边和右手边各有一根筷子。示意图如下:

哲学家必须拿起左右两边的筷子才能进餐,如果他们同时拿起左手边的筷子,就会导致死锁。因为右手边的筷子被他右边的那位哲学家当成左手边的筷子拿起来了,这样一来这五位哲学家谁都没有办法进餐,他们死锁了。
让我们用代码模拟这个死锁:
class Philosopher implements Runnable {
private int id;
public Philosopher(int id) {
this.id = id;
}
public void run() {
int leftCsIndex = id;
int rightCsIndex = (id+1)%5;
synchronized(PhiloTest.chopsticks[leftCsIndex]) {
System.out.println("I got left chopstick");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized(PhiloTest.chopsticks[rightCsIndex]) {
System.out.println("I got right chopstick");
System.out.println("Philosopher"+ id+": eating");
}
}
}
}
public class PhiloTest {
public static Object[] chopsticks = new Object[5];
public static void main(String[] args) {
for(int i=0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0; i < 5; i++) {
exec.execute(new Philosopher(i));
}
exec.shutdown();
}
}
输出结果如下,并且程序始终没有退出:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher4:I got left chopstick
我们创建了一个长度为5的数组,用来模拟筷子。此外我们定义了“哲学家线程”,每个哲学家都有自己的编号,我们假定哲学家左边的筷子对应的是数组中索引和哲学家编号相同的对象,哲学家右边的筷子对应的是数组中索引为哲学家编号加一的对象(注:第4个哲学家右手边的筷子对应数组中第0个对象)。每个哲学家都先拿起左边的筷子,为了保证所有的哲学家都拿到了左边的筷子,每个哲学家拿到左边的筷子后都等待100毫秒,然后再拿起右边的筷子,这时他们死锁了。
死锁的条件
死锁发生有四个条件,必须每个条件都满足才有可能发生死锁,只要破坏其中一个条件就不会死锁。
1互斥:线程申请获得的资源不能共享。在上面的例子中,每个哲学家不和别的哲学家共用一根筷子,反应在代码上就是每个“哲学家线程”用锁实现了互斥,一个哲学家拿到了对象的锁,其它哲学家就不能拿到这个对象的锁了。
2.持有并等待:线程在申请其它资源的时候不释放已经持有的资源。在上面的例子中,哲学家在试图去取右边筷子的时候同时持有左边的筷子。
3.不能抢占:线程持有的资源不能被其它线程抢占。在上面例子中,哲学家只能拿桌子上的筷子,不能从其它哲学家手里抢筷子用。
4.循环等待:在上面的例子中,第0个哲学家在等待第1个哲学家放下筷子,第1个哲学家等第2个哲学家放下筷子....第4个哲学家等待第0个哲学家放下筷子,如此就形成了循环等待。
避免死锁
避免死锁最简单的方法就是打破循环等待,比如5个哲学家中有一个哲学家先去拿右边的筷子,再拿左边的筷子,这样就破坏了循环等待。实例代码如下:
public class SolveDeadLock {
public static Object[] chopsticks = new Object[5];
public static void main(String[] args) {
for(int i=0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0; i < 4; i++) {
exec.execute(new Philosopher(i));
}
exec.shutdown();
int leftCsIndex = 4;
int rightCsIndex = 0;
synchronized(SolveDeadLock.chopsticks[rightCsIndex]) {
System.out.println("Philosopher4:I got right chopstick");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized(SolveDeadLock.chopsticks[leftCsIndex]) {
System.out.println("Philosopher4:I got left chopstick");
System.out.println("Philosopher4: eating");
}
}
}
}
输出结果:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher3:I got right chopstick
Philosopher3: eating
Philosopher2:I got right chopstick
Philosopher2: eating
Philosopher1:I got right chopstick
Philosopher1: eating
Philosopher0:I got right chopstick
Philosopher0: eating
Philosopher4:I got right chopstick
Philosopher4:I got left chopstick
Philosopher4: eating
上面的例子中我们修改了main()方法,使用主线程作为第4个哲学家,第四个哲学家先拿右面的筷子,再拿左面的筷子。这样就避免了循环等待,因此这次没有发生死锁。在哲学家进餐案例中,互斥和持有并等待是不能规避的,因为这两个是逻辑要求的,比如两个哲学家同时使用一根筷子是违背常识的。因此除了第四个条件外,我们还可以通过抢占来规避死锁。比如:设计一个“粗鲁的哲学家”,这个哲学家如果没有拿到筷子,就会去别的哲学家手里面抢筷子,这样就可以保证这个哲学家肯定可以吃到饭,一旦他放下筷子,就只有4个哲学家需要吃饭,而桌子上有5根筷子,这时肯定不会死锁。由于篇幅原因,这里就不使用代码实现了,感兴趣的读者可以试着实现这个想法。
总结
在多线程系统中,许多概率性的问题是由于线程之间发生了死锁,死锁导致一些线程永远都不会停止执行,虽然这些线程一直处于阻塞状态,但是仍然占用内存空间,这样就导致线程所占的内存空间永远不会被释放,这就是传说中的内存泄露。线程死锁是导致Java应用程序发生内存泄露的一个重要原因。因此在编写代码时一定要避免发生死锁,避免死锁最简单的方法就是对资源进行排序,所有线程对资源的访问都按照顺序获取,这样就避免了循环等待,从而避免死锁。
公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。
哲学家必须拿起左右两边的筷子才能进餐,如果他们同时拿起左手边的筷子,就会导致死锁。因为右手边的筷子被他右边的那位哲学家当成左手边的筷子拿起来了,这样一来这五位哲学家谁都没有办法进餐,他们死锁了。
让我们用代码模拟这个死锁:
class Philosopher implements Runnable {
private int id;
public Philosopher(int id) {
this.id = id;
}
public void run() {
int leftCsIndex = id;
int rightCsIndex = (id+1)%5;
synchronized(PhiloTest.chopsticks[leftCsIndex]) {
System.out.println("I got left chopstick");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized(PhiloTest.chopsticks[rightCsIndex]) {
System.out.println("I got right chopstick");
System.out.println("Philosopher"+ id+": eating");
}
}
}
}
public class PhiloTest {
public static Object[] chopsticks = new Object[5];
public static void main(String[] args) {
for(int i=0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0; i < 5; i++) {
exec.execute(new Philosopher(i));
}
exec.shutdown();
}
}
输出结果如下,并且程序始终没有退出:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher4:I got left chopstick
我们创建了一个长度为5的数组,用来模拟筷子。此外我们定义了“哲学家线程”,每个哲学家都有自己的编号,我们假定哲学家左边的筷子对应的是数组中索引和哲学家编号相同的对象,哲学家右边的筷子对应的是数组中索引为哲学家编号加一的对象(注:第4个哲学家右手边的筷子对应数组中第0个对象)。每个哲学家都先拿起左边的筷子,为了保证所有的哲学家都拿到了左边的筷子,每个哲学家拿到左边的筷子后都等待100毫秒,然后再拿起右边的筷子,这时他们死锁了。
死锁的条件
死锁发生有四个条件,必须每个条件都满足才有可能发生死锁,只要破坏其中一个条件就不会死锁。
1互斥:线程申请获得的资源不能共享。在上面的例子中,每个哲学家不和别的哲学家共用一根筷子,反应在代码上就是每个“哲学家线程”用锁实现了互斥,一个哲学家拿到了对象的锁,其它哲学家就不能拿到这个对象的锁了。
2.持有并等待:线程在申请其它资源的时候不释放已经持有的资源。在上面的例子中,哲学家在试图去取右边筷子的时候同时持有左边的筷子。
3.不能抢占:线程持有的资源不能被其它线程抢占。在上面例子中,哲学家只能拿桌子上的筷子,不能从其它哲学家手里抢筷子用。
4.循环等待:在上面的例子中,第0个哲学家在等待第1个哲学家放下筷子,第1个哲学家等第2个哲学家放下筷子....第4个哲学家等待第0个哲学家放下筷子,如此就形成了循环等待。
避免死锁
避免死锁最简单的方法就是打破循环等待,比如5个哲学家中有一个哲学家先去拿右边的筷子,再拿左边的筷子,这样就破坏了循环等待。实例代码如下:
public class SolveDeadLock {
public static Object[] chopsticks = new Object[5];
public static void main(String[] args) {
for(int i=0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0; i < 4; i++) {
exec.execute(new Philosopher(i));
}
exec.shutdown();
int leftCsIndex = 4;
int rightCsIndex = 0;
synchronized(SolveDeadLock.chopsticks[rightCsIndex]) {
System.out.println("Philosopher4:I got right chopstick");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized(SolveDeadLock.chopsticks[leftCsIndex]) {
System.out.println("Philosopher4:I got left chopstick");
System.out.println("Philosopher4: eating");
}
}
}
}
输出结果:
Philosopher0:I got left chopstick
Philosopher2:I got left chopstick
Philosopher1:I got left chopstick
Philosopher3:I got left chopstick
Philosopher3:I got right chopstick
Philosopher3: eating
Philosopher2:I got right chopstick
Philosopher2: eating
Philosopher1:I got right chopstick
Philosopher1: eating
Philosopher0:I got right chopstick
Philosopher0: eating
Philosopher4:I got right chopstick
Philosopher4:I got left chopstick
Philosopher4: eating
上面的例子中我们修改了main()方法,使用主线程作为第4个哲学家,第四个哲学家先拿右面的筷子,再拿左面的筷子。这样就避免了循环等待,因此这次没有发生死锁。在哲学家进餐案例中,互斥和持有并等待是不能规避的,因为这两个是逻辑要求的,比如两个哲学家同时使用一根筷子是违背常识的。因此除了第四个条件外,我们还可以通过抢占来规避死锁。比如:设计一个“粗鲁的哲学家”,这个哲学家如果没有拿到筷子,就会去别的哲学家手里面抢筷子,这样就可以保证这个哲学家肯定可以吃到饭,一旦他放下筷子,就只有4个哲学家需要吃饭,而桌子上有5根筷子,这时肯定不会死锁。由于篇幅原因,这里就不使用代码实现了,感兴趣的读者可以试着实现这个想法。
总结
在多线程系统中,许多概率性的问题是由于线程之间发生了死锁,死锁导致一些线程永远都不会停止执行,虽然这些线程一直处于阻塞状态,但是仍然占用内存空间,这样就导致线程所占的内存空间永远不会被释放,这就是传说中的内存泄露。线程死锁是导致Java应用程序发生内存泄露的一个重要原因。因此在编写代码时一定要避免发生死锁,避免死锁最简单的方法就是对资源进行排序,所有线程对资源的访问都按照顺序获取,这样就避免了循环等待,从而避免死锁。
公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。
Java并发编程(十)死锁的更多相关文章
- Java并发编程 (十) 多线程并发拓展
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.死锁 1.死锁的定义 所谓的死锁是指两个或两个以上的线程在等待执行的过程中,因为竞争资源而造成的一种 ...
- 【Java并发编程】并发编程大合集-值得收藏
http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用 ...
- 【Java并发编程】并发编程大合集
转载自:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅 ...
- Java并发编程原理与实战四十二:锁与volatile的内存语义
锁与volatile的内存语义 1.锁的内存语义 2.volatile内存语义 3.synchronized内存语义 4.Lock与synchronized的区别 5.ReentrantLock源码实 ...
- Java并发编程原理与实战十一:锁重入&自旋锁&死锁
一.锁重入 package com.roocon.thread.t6; public class Demo { /* 当第一个线程A拿到当前实例锁后,进入a方法,那么,线程A还能拿到被当前实例所加锁的 ...
- java并发编程如何预防死锁
在java并发编程领域已经有技术大咖总结出了发生死锁的条件,只有四个条件都发生时才会出现死锁: 1.互斥,共享资源X和Y只能被一个线程占用 2.占有且等待,线程T1已经取得共享资源X,在等待共享资源Y ...
- java并发编程笔记(十)——HashMap与ConcurrentHashMap
java并发编程笔记(十)--HashMap与ConcurrentHashMap HashMap参数 有两个参数影响他的性能 初始容量(默认为16) 加载因子(默认是0.75) HashMap寻址方式 ...
- java并发编程笔记(八)——死锁
java并发编程笔记(八)--死锁 死锁发生的必要条件 互斥条件 进程对分配到的资源进行排他性的使用,即在一段时间内只能由一个进程使用,如果有其他进程在请求,只能等待. 请求和保持条件 进程已经保持了 ...
- Java并发编程实战 04死锁了怎么办?
Java并发编程文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 Java并发编程实战 03互斥锁 解决原子性问题 前提 在第三篇 ...
- java并发编程JUC第十篇:CyclicBarrier线程同步
在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...
随机推荐
- <Android 应用 之路> MPAndroidChart~BubbleChart(气泡图) and RadarChart(雷达图)
简介 MPAndroidChart是PhilJay大神给Android开发者带来的福利.MPAndroidChart是一个功能强大并且使用灵活的图表开源库,支持Android和iOS两种,这里我们暂时 ...
- VC++中如何将字符串转换成整型数字
原文:http://blog.csdn.net/yongf2014/article/details/47071663 注意: atoi函数是c的函数,它的输入参数是char *类型. 你声明了stri ...
- 浏览器缓存(Web Cache/ Http Cache)
浏览器缓存(Web Cache/ Http Cache)是前端性能优化中很重要的组成部分. 缓存策略 浏览器的缓存策略是: 1.如果本地没有缓存,则发送非条件性请求. 2.如果本地有缓存,则判断本地缓 ...
- Software Testing Techniques LAB 02: Selenium
1. Installing 1. Install firefox 38.5.1 2. Install SeleniumIDE After installing, I set the view o ...
- 关于webWorker的理解和简单例子
一.理解 当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成. web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能.您可以继续做任何愿 ...
- Linux Swap扩容
最近在做rac,在环境检查的时候发现swap空间检查不通过,所以我们第一想到的是对swap进行扩容,那么swap扩容有哪些方法呢?这里主要介绍两种方法,一通过添加额外磁盘,扩展swap分区,二是通过本 ...
- Exchange 接收连接器(Client、Default)区别,OUtlook实际测试
CAS就是接收连接器(110,995): Server Config--Client Access:POP3 and IMAP4:POP3设置 HUB就是发送连接器(25,587) Server Co ...
- Python学习---IO模型1227
1.1. 事件驱动 事件驱动属于一种编程的范式,一种编程的风格,它擅长于处理一些未知的事件,通过绑定一个事件,外界触发后激活这个事情,达到执行某些操作的目的.比如浏览器的onclick()事件 1.2 ...
- Linux kill/pkill/killall命令详解
kill kill(terminate a process)命令用来终止指定的进程, 对于一个后台进程就须用kill命令来终止,我们就需要先使用ps/pidof/pstree/top等工具获取进程PI ...
- c++我在努力----第三次作业体会
[github地址](https://github.com/kobe96/object-oriented) 这次作业的三大难点 1.类的应用 从我们并没有接触过c++,到掌握如何写一个程序,多个类的引 ...