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应用程序发生内存泄露的一个重要原因。因此在编写代码时一定要避免发生死锁,避免死锁最简单的方法就是对资源进行排序,所有线程对资源的访问都按照顺序获取,这样就避免了循环等待,从而避免死锁。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

原文地址:https://www.cnblogs.com/victorwux/p/9218363.html

时间: 2024-10-04 22:28:55

Java并发编程(十)死锁的相关文章

Java 并发编程之死锁

动态的锁顺序死锁 这是接着上一篇的写.为了方便又贴了一遍代码,因为第二天我发现,这个通过锁顺序来避免死锁的程序依旧有问题. 我的问题是: 一个对象的Object的Hashcode的值 应该是固定的,那么.虽然这个代码通过hashcode规范了锁顺序,当两个人互相往对方的账户里面转账的时候.不还是变成了 public void transferMoney(Account formaccaount, Account toaccount, DollarAmount amount) { synchron

【Java并发编程】并发编程大合集-值得收藏

http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [Java并发编程]守护线程和线程阻塞    [Ja

【Java并发编程】并发编程大合集

转载自:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章    [Java并发编程]实现多线程的两种方法    [Java并发编程]线程的中断    [Java并发编程]正确挂起.恢复.终止线程    [Java并发编程]守护线程和线程阻塞    [Java并发编程]Volatile关键字(上)

《Java并发编程实战》第十六章 Java内存模型 读书笔记

Java内存模型是保障多线程安全的根基,这里仅仅是认识型的理解总结并未深入研究. 一.什么是内存模型,为什么需要它 Java内存模型(Java Memory Model)并发相关的安全发布,同步策略的规范.一致性等都来自于JMM. 1 平台的内存模型 在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏或栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证. JVM通过在适当的位置上插入内存栅栏来屏蔽在JVM与底层平台内存模型之间的

《Java并发编程实战》第十五章 原子变量与非阻塞同步机制 读书笔记

一.锁的劣势 锁定后如果未释放,再次请求锁时会造成阻塞,多线程调度通常遇到阻塞会进行上下文切换,造成更多的开销. 在挂起与恢复线程等过程中存在着很大的开销,并且通常存在着较长时间的中断. 锁可能导致优先级反转,即使较高优先级的线程可以抢先执行,但仍然需要等待锁被释放,从而导致它的优先级会降至低优先级线程的级别. 二.硬件对并发的支持 处理器填写了一些特殊指令,例如:比较并交换.关联加载/条件存储. 1 比较并交换 CAS的含义是:"我认为V的值应该为A,如果是,那么将V的值更新为B,否则不需要修

转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)

简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接口有3个实现它的类:ReentrantLock.ReetrantReadWriteLock.ReadLock和ReetrantReadWriteLock.WriteLock,即重入锁.读锁和写锁.lock必须被显式地创建.锁定和释放,为了可以使用更多的功能,一般用ReentrantLock为其实例化

转: 【Java并发编程】之十八:第五篇中volatile意外问题的正确分析解答(含代码)

转载请注明出处:http://blog.csdn.net/ns_code/article/details/17382679 在<Java并发编程学习笔记之五:volatile变量修饰符-意料之外的问题>一文中遗留了一个问题,就是volatile只修饰了missedIt变量,而没修饰value变量,但是在线程读取value的值的时候,也读到的是最新的数据.但是在网上查了很多资料都无果,看来很多人对volatile的规则并不是太清晰,或者说只停留在很表面的层次,一知半解. 这两天看<深入Ja

JAVA 并发编程-线程同步通信技术(Lock和Condition)(十)

在之前的博客中已经介绍过线程同步通信技术<JAVA 并发编程-传统线程同步通信技术(四)>,上篇是使用的synchronized,wait,notify来实现,今天我们使用的是Lock和Condition,下面我们结合两者对比来学习. 简单的Lock锁应用: /** * 简单Lock的应用 * @author hejingyuan * */ public class LockTest { public static void main(String[] args) { new LockTest

JAVA并发编程艺术 一(并发编程的挑战)

从今天起开始java并发编程艺术的学习,每一章学习完以后再这里记录下内容的重点,做个笔记,加深印象. 并发编程的目的是为了让程序运行的更快,但是,并不是启动更多的线程就能让程序最大限度地并发执行.在进行并发是,如果希望通过多现场执行任务让程序运行得更快,会面临非常多的挑战,比如上下文切换的问题,死锁的问题,以及受限于硬件和软件的资源限制问题,本章会介绍几种并发编程的挑战以及解决方案 1.上下问切换 即使是单核处理器也支持多线程执行代码,cpu通过给每个线程分配cpu时间片来实现这个机制.时间片是

《Java并发编程实战》要点笔记及java.util.concurrent 的结构介绍

买了<java并发编程实战>这本书,看了好几遍都不是很懂,这个还是要在实战中找取其中的要点的,后面看到一篇文章笔记做的很不错分享给大家!! 原文地址:http://blog.csdn.net/cdl2008sky/article/details/26377433 Subsections  1.线程安全(Thread safety) 2.锁(lock) 3.共享对象 4.对象组合 5.基础构建模块 6.任务执行 7.取消和关闭 8.线程池的使用 9.性能与可伸缩性 10.并发程序的测试 11.显