面试官:“遇到过死锁问题吗?怎么发生的?如何解决呢?“

本文详细探讨了死锁的概念、常见场景,包括锁顺序死锁、多个对象协作产生的死锁和线程饥饿死锁,并给出了Android系统处理死锁的方案,以及在Android开发中分析和预防死锁的方法,如借助Android Studio的调试工具和ANR机制。
摘要由CSDN通过智能技术生成

在多线程场景下,如果对锁资源的处理不当,就可能导致死锁。而当发生死锁时,多数情况也无法实时解决,都是需要重启来解决问题的。所以针对死锁问题,都是以预防为主。

今天推荐一篇来自搜狐视频团队的讲解 Android 死锁的文章,全面了解死锁产生的原因,以及如何预防死锁。

一、什么是死锁

说到死锁,大家可能都不陌生,每次遇到死锁,总会让计算机产生比较严重的后果,比如资源耗尽,界面无响应等。

死锁的精确定义:

集合中的每一个进程(或线程)都在等待只能由本集合中的其他进程(或线程)才能引发的事件,那么该组进程是死锁的。

对于这个定义大家可能有点迷惑,换一种通俗的说法就是:

死锁是指两个或两个以上的线程,在执行过程中,由于竞争资源或者由于彼此通信,而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

经典的「哲学家进餐问题」可以帮助我们形象的理解死锁问题。

有五个哲学家,他们的生活方式是交替地进行思考和进餐,哲学家们共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五支筷子,平时哲学家进行思考,饥饿时便试图取其左、右最靠近他的筷子,只有在他拿到两支筷子时才能进餐,该哲学家进餐完毕后,放下左右两只筷子又继续思考。

当五个哲学家同时去取他左边的筷子,每人拿到一只筷子且不释放,即五个哲学家只得无限等待下去,这样就产生了死锁的问题。

在计算机中也可以用有向图来描述死锁问题,首先假定每个线程为有向图中的一个节点, 申请锁的线程 A 为起点, 拥有锁的线程 B 为终点,这样就形成线程 A 到线程 B 的一条有向边,而众多的锁 (边) 和线程(点), 就构成了一个有向图

如果在有向图中形成一条环路,就会产生一个死锁,如上图所示。在很多计算机系统中,检测是否有死锁存在,就是将问题抽象为寻找有向图中的环路。

二、常见的死锁的场景

下面分析几种常见的死锁形式:

2.1 锁顺序死锁

 

public class TestDeadLock {
  private final Object lockA = new Object();
  private final Object lockB = new Object();
  public void lockAtoB(){
    synchronized (lockA){
      synchronized (lockB){
        doSomething();
      }
    }
  }
  public void lockBtoA(){
    synchronized (lockB){
      synchronized (lockA){
        doSomething();
      }
    }
  }
  private void doSomething(){
    System.out.println("doSomething");
  }
}

上述代码中,如果一个线程调用 lockAtoB(),另一个线程调用 lockBtoA(),并且两个线程是交替执行,那么在程序运行期间是有一定几率产生死锁。

而产生死锁的原因是:两个线程用不同的顺序去获取两个相同的锁,如果可以始终用相同的顺序,即每个线程都先获取 lockA,然后再获取 lockB,就不会出现循环的加锁依赖,也就不会产生死锁。

当然上面的代码只是一个示例,实际的代码中不会这么简单,而有些函数中,虽然看似都是以相同的顺序加锁,但是由于外部调用的不确定性,仍然会导致实际以不同的顺序加锁而产生死锁。

再看一个例子:

//仓库
public static interface IStore{
  public void inCome(int count);
  public void outCome(int count);
}
/**
 * 从 in 仓库 调用货物去 out仓库
 * @param from
 * @param to
 * @param count 调用货物量
 */
public void transportGoods(IStore from,IStore to,int count){
  synchronized (from){
    synchronized (to){
      from.outCome(count);//出仓库
      to.inCome(count);//入仓库
    }
  }
}

 货运公司将货物从一个仓库转运到另一个仓库,转运前,需要同时获得两个仓库的锁,以确保两个仓库中的货物数量是以原子方式更新。看起来这个函数都是以相同的顺序获取锁,但这只是函数内部的顺序,而真正的执行顺序,取决于外部传入的对象。

transportGoods(storeA,storeB,100);
transportGoods(storeB,storeA,40);

 

如果用上述代码调用,在频繁的调用过程中,也很容易产生死锁。从上面的代码中可以看出,需要一个方法来确保在整个程序运行期间,锁都按照事先定义好的顺序来获取。这里提供一种方式:通过比较对象的 hashcode 值,来定义锁的获取顺序。

下面来改造一下上述代码。

private static final Object extraLock = new Object();
/**
 * 从 in 仓库 调用货物去 out仓库
 * @param from
 * @param to
 * @param count 调用货物量
 */
public void transportGoods(IStore from,IStore to,int count){
  int fromHashCode = System.identityHashCode(from);
  int toHashCode = System.identityHashCode(to);
  if(fromHashCode > toHashCode){
    synchronized (from){
      synchronized (to){
          transportGoodsInternal(from,to,count);
      }
    }
}else if(fromHashCode < toHashCode){
    synchronized (to){
        synchronized (from){
            transportGoodsInternal(from,to,count);
        }
    }
  }else {//hash散列冲突,需要用新的一个锁来保证这种低概率情况下不出现问题
    synchronized (extraLock){
      synchronized (from){
        synchronized (to){
          transportGoodsInternal(from,to,count);
        }
      }
    }
  }
}
public void transportGoodsInternal(IStore from,IStore to,int count){
  from.outCome(count);//出仓库
  to.inCome(count);//入仓库
}

 

上述代码不难理解,使用 hashcode 的大小来唯一确定锁的顺序,需要值得注意的是,使用 identityHashCode

而不是对象自身的 hashCode 方法,这样可以降低用户重写 hashcode 后带来的冲突风险。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值