多线程之死锁

多线程之死锁

一、什么是死锁

当两个或两个以上正在执行的线程在同时抢夺资源,并且处于一种互相等待的状态,由于存在一种环路的锁依赖关系而永远等待下去,没有外界原因终止程序或者重启程序 它们将永远执行等待下去,称之为死锁。

经典的“哲学家进餐”的问题,很好的阐述了死锁的概念

5个哲学家坐在一起进餐,但是只有5支筷子(不是5双),并且每两个人中间都放有一支筷子,哲学家处于两种状态,要么思考,要么进餐,每个人都需要一双筷子才能吃东西,并且吃完后需要将两支筷子放回原来的位置继续思考,有的筷子管理算法,可以让每个人都可以吃到东西,但是有一些算法却可能导致所有的哲学家“饿死”,后者的情况就是死锁,每个人都拥有其他人需要的资源,同时在等待其他人已拥有的资源,并且每个人在获取新资源之前不会放开自己已有的资源。

筷子管理算法:一个饥饿的科学家会尝试获得两根临近的筷子,但如果其中一根正在被另一个科学家使用,那么他将放弃已经得到的
那根筷子,并在等待几分钟之后尝试

死锁:每个人都立即抓住自己左边的筷子,然后等待自己右边的筷子空出来,但是等待的同时又不放下已经拿到的筷子,形成5个人互相等待的状态

饥饿:哲学家都同时想吃饭,同时拿起左边的筷子,但是发现右边没有筷子,于是又同时放下左手边的筷子,然后大家发现左边又有筷子了,又同时拿起来了,然后反复进行。好,饿死了
哲学家进餐
在A线程持有L锁的时候想获得M锁的同时,B线程持有M锁想获得L锁,那么这两个线程将会一直等待下去,这种情况成为死锁(或者称为“抱死”)
在这里插入图片描述

二,死锁的四个必要条件

  1. 互斥条件 :排它性,即在一段时间内某资源只有一个进程所使用,如果此时有其他进程想使用该资源,则请求者只能等待,等待这个资源被前一个进程使用完毕之后并且释放
  2. 请求和保持条件:指一个进程已拥有一个资源又尝试去请求另一个资源,而该资源已被其他进程所占有,此时请求进程阻塞,在请求其他资源的同时不释放已拥有的资源。
  3. 不剥夺条件 :在一个进程获得资源,在未使用完毕释放先,不可被剥夺,必须是自己释放
  4. 环路等地条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{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();

    @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();


    }
}

当DeadLock 类的对象flag=1时(td1),先锁定o1,睡眠500毫秒
而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
td1\td

动态锁顺序死锁:

// 资金转账到账号
    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和B线程都分别持有对方线程尝试要得到的锁,A获得了X,尝试去锁住Z;B获得了Z又尝试去获得X,导致循环锁的成立,就会发生死锁:
在这里插入图片描述
设置加锁顺序就是,两个线程按照不同的顺序获得锁,如果按照相同的顺序就会发生循环加锁(死锁):
在这里插入图片描述
这样就永远不会发生死锁,针对两个特定的锁,可以按照锁住两个对象hashCode值得大小顺序,这样就会以不同的顺序获得锁,我们通过设定锁的顺序,来防止发生死锁,这里我们使用System.identityHashCode来定义锁的顺序,这个方法将以Object.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();
                    }
                }
            }
        }
    }

}
4.2定时锁的机制

有一项技术可以检测死锁和从死锁中恢复过来,就是使用Lock类中的定时功能public boolean tryLock(long time, TimeUnit unit) throws InterruptedException 来代替内置锁机制,内置锁的机制就是如果没有获得锁就永远等待下去,而tryLock 可以指定一个超时时间,如果超过超时时间会返回失败
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值