Java多线程死锁避免方法

一、什么是死锁
当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。
经典的 “哲学家进餐” 问题很好地描述了死锁状况:
5个哲学家去吃中餐,坐在一张圆桌旁,他们有5根筷子(而不是5双),并且每两个人中间放一根筷子,哲学家们要么在思考,要么
在进餐,每个人都需要一双筷子才能吃到东西,并在吃完后将筷子放回原处继续思考,有些筷子管理算法 (1) 能够使每个人都能相对及
时的吃到东西,但有些算法却可能导致一些或者所有哲学家都"饿死",后一种情况将产生死锁:每个人都拥有其他人需要的资源,
同时有等待其他人已经拥有的资源,并且每个人在获取所有需要的资源之前都不会放弃已经拥有的资源。
筷子管理算法(1):一个饥饿的科学家会尝试获得两根临近的筷子,但如果其中一根正在被另一个科学家使用,那么他将放弃已经得到的
那根筷子,并在等待几分钟之后尝试
死锁:每个人都立即抓住自己左边的筷子,然后等待自己右边的筷子空出来,但同时又不放下已经拿到的筷子,形成一种相互等待的状态。
饥饿:哲学家们都同时想吃饭,同时拿起左手边筷子,但是发现右边没有筷子,于是哲学家又同时放下左手边筷子,然后大家发现又有筷子了,又同时开始拿起左手边筷子,又同时放下,然后反复进行。
在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去,这种情况就是死锁形式(或者称为"抱死").
二、死锁的四个必要条件
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
请求和保持条件:指进程XM代理申请www.kaifx.cn/broker/xm.html已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
三、死锁实例
/**

  • 死锁类示例
    */
    public class DeadLock implements Runnable {
    public int flag = 1;
    //静态对象是类的所有对象共享的
    private static Object o1 = new Object(), o2 = new Object();br/>@Override
    public void run() {
    System.out.println("flag:{}"+flag);
    if (flag == 1) { //先锁o1,再对o2加锁,环路等待条件
    synchronized (o1) {
    try {
    Thread.sleep(500);
    } catch (Exception e) {
    e.printStackTrace();
    }
    synchronized (o2) {
    System.out.println("1");
    }
    }
    }
    if (flag == 0) {//先锁o2,在锁01
    synchronized (o2) {
    try {
    Thread.sleep(500);
    } catch (Exception e) {
    e.printStackTrace();
    }
    synchronized (o1) {
    System.out.println("0");
    }
    }
    }
    }
    public static void main(String[] args) {
    DeadLock td1 = new DeadLock();
    DeadLock td2 = new DeadLock();
    td1.flag = 1;
    td2.flag = 0;
    //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
    //td2的run()可能在td1的run()之前运行
    new Thread(td1).start();
    new Thread(td2).start();
    }
    }
    1、当DeadLock 类的对象flag=1时(td1),先锁定o1,睡眠500毫秒
    2、而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
    3、td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
    4、td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
    5、td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
    动态锁顺序死锁:
    // 资金转账到账号
    public static void transferMoney(Account fromAccount,
    Account toAccount,
    DollarAmount amount)
    throws InsufficientFundsException {
    // 锁定汇款者的账户
    synchronized (fromAccount) {
    // 锁定到账者的账户
    synchronized (toAccount) {
    // 判断账户的余额不能为负数
    if (fromAccount.getBalance().compareTo(amount) < 0) {
    throw new InsufficientFundsException();
    } else {
    // 汇款者的账户减钱
    fromAccount.debit(amount);
    // 到账者的账户增钱
    toAccount.credit(amount);
    }
    }
    }
    }
    上面的代码看起来都是按照相同的顺序来获得锁的,按道理来说是没有问题,但是上述代码中上锁的顺序取决于传递给transferMoney()的参数顺序,而这些参数顺序又取决于外部的输入
    如果两个线程(A和B)同时调用transferMoney()
    其中一个线程(A),从X向Y转账:transferMoney(myAccount,yourAccount,10);
    另一个线程(B),从Y向X转账 :transferMoney(yourAccount,myAccount,20);
    此时 A线程 可能获得 myAccount 的锁并等待 yourAccount的锁,然而 B线程 此时已经持有 yourAccount 的锁,并且正在等待 myAccount 的锁,这种情况下就会发生死锁。
    当一组java线程发生死锁的时候,那么这些线程永远不能再使用了,根据线程完成工作的不同,可能会造成应用程序的完全停止,或者某个特定的子系统不能再使用了,或者是性能降低,这个时候恢复应用程序的唯一方式就是中止并重启它,死锁造成的影响很少会立即显现出来,如果一个类发生死锁,并不意味着每次都会发生死锁,而只是表示有可能,当死锁出现的时候,往往是在最糟糕的时候——在高负载的情况下。
    四、死锁的避免与检测
    4.1 预防死锁
    破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁
    破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
    破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
    破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
    4.2 设置加锁顺序
    如果两个线程(A和B),当A线程已经锁住了Z,而又去尝试锁住X,而X已经被线程B锁住,线程A和线程B分别持有对应的锁,而又去争夺其他一个锁(尝试锁住另一个线程已经锁住的锁)的时候,就会发生死锁
    这样死锁就永远不会发生。 针对两个特定的锁,可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,我们通过设置锁的顺序,来防止死锁的发生,在这里我们使用System.identityHashCode方法来定义锁的顺序,这个方法将返回由Obejct.hashCode 返回的值,这样就可以消除死锁发生的可能性。
    public class DeadLockExample3 {
    // 加时赛锁,在极少数情况下,如果两个hash值相等,使用这个锁进行加锁
    private static final Object tieLock = new Object();
    public void transferMoney(final Account fromAcct,
    final Account toAcct,
    final DollarAmount amount)
    throws InsufficientFundsException {
    class Helper {
    public void transfer() throws InsufficientFundsException {
    if (fromAcct.getBalance().compareTo(amount) < 0)
    throw new InsufficientFundsException();
    else {
    fromAcct.debit(amount);
    toAcct.credit(amount);
    }
    }
    }
    // 得到两个锁的hash值
    int fromHash = System.identityHashCode(fromAcct);
    int toHash = System.identityHashCode(toAcct);
    // 根据hash值判断锁顺序,决定锁的顺序
    if (fromHash < toHash) {
    synchronized (fromAcct) {
    synchronized (toAcct) {
    new Helper().transfer();
    }
    }
    } else if (fromHash > toHash) {
    synchronized (toAcct) {
    synchronized (fromAcct) {
    new Helper().transfer();
    }
    }
    } else {// 如果两个对象的hash相等,通过tieLock来决定加锁的顺序,否则又会重新引入死锁——加时赛锁
    synchronized (tieLock) {
    synchronized (fromAcct) {
    synchronized (toAcct) {
    new Helper().transfer();
    }
    }
    }
    }
    }
    }
    在极少数情况下,两个对象可能拥有两个相同的散列值,此时必须通过某种任意的方法来决定锁的顺序,否则可能又会重新引入死锁。
    为了避免这种情况,可以使用 “加时(Tie-Breaking))”锁,这获得这两个Account锁之前,从而消除了死锁发生的可能性
    4.3 支持定时的锁(超时放弃)
    有一项技术可以检测死锁和从死锁中恢复过来,就是使用Lock类中的定时public boolean tryLock(long time, TimeUnit unit) throws InterruptedException功能,来代替内置锁机制,当使用内置锁的时候,只要没有获得锁,就会永远等待下去,而tryLock可以指定一个超时时间(Timeout),在等待超过时间后tryLock会返回一个失败信息,如果超时时限比获取锁的时间要长很多,那么就可以在发生某个意外后重新获得控制权。如下图所示:
    4.4 死锁避免
    死锁防止方法能够防止发生死锁,但必然会降低系统并发性,导致低效的资源利用率,其中最具有代表性的避免死锁算法是银行家算法。
    1、多个资源的银行家算法
    检查一个状态是否安全的算法如下:
    查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
    假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
    重复以上两步,直到所有进程都标记为终止,则状态时安全的。
    如果一个状态不是安全的,需要拒绝进入这个状态。
    4.5 死锁检测
    对资源的分配加以适当限制可防止或避免死锁发生,但不利于进程对系统资源的充分共享。
    为每个进程和每个资源指定一个唯一的号码
    Jstack命令
    jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,
    生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待,线程
    出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
    JConsole工具
    Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资
    源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。
    4.5 死锁恢复
    资源剥夺:剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除
    进程回退:根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制
    进程撤销:
    1、撤销陷入死锁的所有进程,解除死锁,继续运行。
    2、逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。
    可选择符合下面条件之一的先撤销:
    1.CPU消耗时间最少者
    2.产生的输出量最小者
    3.预计剩余执行时间最长者
    4.分得的资源数量最少者后优先级最低者
    系统重启:结束所有进程的执行并重新启动操作系统。这种方法很简单,但先前的工作全部作废。

Java多线程死锁避免方法

原文地址:https://blog.51cto.com/14511863/2442532

时间: 2024-11-06 15:09:07

Java多线程死锁避免方法的相关文章

Java线程死锁解决方法(转)

转自:http://leowzy.iteye.com/blog/740859 Java线程死锁如何避免这一悲剧  Java线程死锁需要如何解决,这个问题一直在我们不断的使用中需要只有不断的关键.不幸的是,使用上锁会带来其他问题.让我们来看一些常见问题以及相应的解决方法: Java线程死锁 Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成.假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭.他们都需要获得两个锁:共享

java 多线程死锁问题

什么是死锁 所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去. 死锁产生的原因及条件 那么为什么会产生死锁呢? 1.因为系统资源不足. 2.进程运行推进的顺序不合适. 3.资源分配不当. 学过操作系统的朋友都知道:产生死锁的条件有四个: 1.互斥条件:所谓互斥就是进程在某一时间内独占资源. 2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放. 3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺

java多线程死锁

原文链接 作者:Jakob Jenkov 译者:申章 校对:丁一 java中死锁是两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁.死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候. 例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了.线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情.为了得到彼此的对象(A和B),它们将永远阻塞下去.这种情况就是一个死锁. 该情况如下: Thread 1

Java多线程同步的方法

一 synchronized关键字 1.synchronized实现原理: ---基于对象监视器(锁) java中所有对象都自动含有单一的锁,JVM负责跟踪对象被加锁的次数.如果一个对象被解锁,其计数变为0.在任务(线程)第一次给对象加锁的时候, 计数变为1.每当这个相同的任务(线程)在此对象上获得锁时,计数会递增.只有首先获得锁的任务(线程)才能继续获取该对象上的多个锁.每当任务离开时,计数递减,当计数为0的时候,锁被完全释放. Java中每个对象或者类都有一把锁与之相关联,对于对象来说,监视

java多线程的实现方法

java虚拟机允许应用程序并发地运行多个线程.在java语言中,多线程的实现一般有以下三种方法: 1)实现Runnable接口,并实现该接口的run()方法 class MyThread implements Runnable { public void run(){ System.out.println("Thread Body"); } } public class Test { public static void main(String[] args) { MyThread t

Java 查看死锁的方法

那我们怎么确定一定是死锁呢?有两种方法. 1>使用JDK给我们的的工具JConsole,可以通过打开cmd然后输入jconsole打开. 1)连接到需要查看的进程. 2)打开线程选项卡,然后点击左下角的“检测死锁” 3)jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情: 从上图中我们可以看出: ①在线程Thread-1中,从状态可以看出,它想申请Paper这个资源,但是这个资源已经被Thread-0拥有了,所以就堵塞了. ②在线程Thread-0中,从状态可以看出,它

Java 多线程的实现方法

package com.jckb; /**多线程实现的两种方法 * * @author gx * */ public class Test2 { public static void main(String[] args) { Mythread m = new Mythread(); m.start();// 不能直接调用run方法 // m.run();//是方法调用,不是线程的启动 Thread t = new Thread(new Mythread2()); t.start(); } }

Java多线程之~~~~synchronized 方法

在多线程开发中,总会遇到多个在不同线程中的方法操作同一个数据,这样在不同线程中操作这个数据不同的顺序 或者时机会导致各种不同的现象发生,以至于不能实现你预期的效果,不能实现一致性,这时候就可以使用 synchronized关键字对一个方法来说,这个synchronized能保证所有调用这个方法的线程只有一个正在操作这个方法, 不会出现同时多个线程进入这个方法的情况,下面我们来一个例子说明这个情况. 首先是一个Account类,这个类模拟账户,提供增加工资和减少工资的方法,当然,这个方法是被syn

java多线程----死锁

什么叫死锁?所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.    那么为什么会产生死锁呢?1.因为系统资源不足.2.进程运行推进的顺序不合适.    3.资源分配不当.             学过操作系统的朋友都知道:产生死锁的条件有四个:1.互斥条件:所谓互斥就是进程在某一时间内独占资源.2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放.3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行