Android常见死锁情况与处理方案

一、什么是死锁

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

死锁的精确定义:

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

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

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

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

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

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

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

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

二、常见的死锁的场景

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

锁顺序死锁

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后带来的冲突风险。具体可参考 System.identityHashCode(obj) 与 obj.hashcode()的区别。当然使用identityHashCode也不能完全避免冲突,当identityHashCode也冲突的时候,引入了额外的一个锁extraLock,这个锁是static的,也就是说,整个应用程序只有一个,虽然理论上整个程序都使用一个 extraLock可能会导致并发性能的下降,但是考虑实际情况下,identityHashCode冲突的可能性非常小,所以并发性能问题也将不是问题。

那么如果能从业务的层面找到IStore中唯一的,不可变的编码,例如,仓库在数据库中的唯一编码,就可以不使用hashcode了,也可以避免使用extraLock。当然这需要大家通过实际的业务逻辑来进行分析提取这个唯一编码。

需要注意的是,使用hashcode这种方式是兼容性最好,成本最低也最不容易出错的方式,如果使用自有编码,你需要确保编码的唯一性,不可变性,这要保证这一点很不容易。

多个对象协作发生的死锁

之前讨论的死锁发生在一个对象内部,这样的死锁问题,比较明显,也容易发现。当互相调用的类变为两个或者更多,而两个类中又分别有各自加锁同步的逻辑,这样的死锁隐藏在代码逻辑中,不容易发现,也不容易寻找。首先来看一个例子。

/**
 * 玩游戏者
 */
class Player {
    private SystemMonitor monitor;
    private int cardCount;//收集的卡片的数量

    public Player(SystemMonitor monitor) {
        this.monitor = monitor;
    }
    
    public synchronized int getCardCount() {
        return cardCount;
    }
    
    public synchronized void collectCard(int count){
        cardCount += count;
        if(cardCount >= 50){
            monitor.notifyComplete(this);
        }
    }
}

/**
 * 监控系统
 */
class SystemMonitor {
    private ArrayList<Player> playerArrayList;//所有玩家
    private ArrayList<Player> completePlayerArrayList = new ArrayList<>();//完成的玩家

    //通知监控系统完成
    public synchronized void notifyComplete(Player player){
        System.out.println("玩家完成收集");
        completePlayerArrayList.add(player);
    }
    //实时监控大家手中牌的数量
    public synchronized void monitorAllPlayer(){
        for (Player player : playerArrayList){
            System.out.println("玩家有"+ player.getCardCount() + "张牌");
        }
    }
}

Player代表玩家,玩家收集完成,50张牌后通知监控系统自己完成游戏,而监控系统通过monitorAllPlayer来实时监控玩家目前手中的牌的数量。不难理解,在Player和SystemMonitor的方法中加锁,是为了避免数据的不一致性。粗略看这一段代码时,没有任何方法会显式的获取两个锁。但是collectCard方法与monitorAllPlayer方法由于调用了外部类的方法,所以他们其实是会拥有两个锁的。假设这样一种情形,当一个玩家收集满50张牌,他通知监控系统他已完成收集,玩家先后获取了Player对象的锁与SystemMonitor对象的锁,而这个时候,监控系统正在扫描所有玩家,而监控系统会先获取自身的锁,然后再获取玩家的锁。这样就有可能出现在两个线程中获取锁顺序不一致的情况,因此就有可能产生死锁。

当一个对象的方法在持有锁期间调用外部方法,这时应该格外注意,因为无法显式判断外部方法是否有其他锁,而这样就有可能产生死锁。

针对上述描述,该如何避免死锁呢?

首先引入一个术语开放调用,即调用某个方法的时候,不需要持有锁,这种调用称为开放调用。通过尽可能地使用开放调用,更容易找出其他锁的路径,也更容易保证加锁的顺序,以此来避免死锁问题。

上述的代码很容易修改为开放调用,此时需要做的就是缩小锁的粒度,使得同步方法只用来保护真正需要保护的变量或者代码段。

public void collectCard(int count){
    boolean isComplete = false;
    synchronized (this){
       cardCount += count;
       if(cardCount >= 50){
           isComplete = true;
       }
    }
    if(isComplete){
        monitor.notifyComplete(this);
    }
}


//实时监控大家手中牌的数量
public void monitorAllPlayer(){
    ArrayList<Player> copy;
    synchronized (this){
        copy = new ArrayList<>(playerArrayList);
    }
    for (Player player : copy){
        System.out.println("玩家有"+ player.getCardCount() + "张牌");
    }
}

线程饥饿死锁

在线程池中,如果任务依赖于其他任务,就可能产生死锁。举一个简单的单线程Executor的例子,如果任务A已经在Executor中运行,而任务A又向相同的Executor中提交了一个任务B,通常情况下,这样会产生死锁。任务B在队列中一直等待任务A完成,而任务A由于是在单线程Executor中,所以又在等待任务B执行完成,这样就造成了死锁。在更大的线程池中,考虑极限情况,如果所有正在执行任务的线程,都在等待之前提交到线程池中排队的任务,这样线程会永远等待下去,这种问题称为线程饥饿死锁。下面的代码展示了线程饥饿死锁。

private ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5,0,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());;

@Test
public void test() throws ExecutionException, InterruptedException {
    int count = 0;
    while (true) {
        System.out.println("开始 = " + (count));
        start();
        System.out.println("结束 = " + (count++));
        Thread.sleep(10);
    }
}

public void start() throws ExecutionException, InterruptedException {
    Callable<String> second = new Callable<String>() {
        @Override
        public String call() throws Exception {
            Thread.sleep(100);
            return "second callable";
        }
    };

    Callable<String> first = new Callable<String>() {
        @Override
        public String call() throws Exception {
            Thread.sleep(10);
            Future<String> secondFuture = executor.submit(second);
            String secondResult = secondFuture.get();
            Thread.sleep(10);
            return "first callable. second result = " + secondResult;
        }
    };

    List<Future<String>> futures = new ArrayList<>();
    for(int i = 0; i< 5; i++){
        System.out.println("submit : " + i);
        Future<String> firstFuture = executor.submit(first);
        futures.add(firstFuture);
    }
    for(int i = 0; i < 5; i++){
        String firstrResult = futures.get(i).get();
        System.out.println(firstrResult + ":" + i);
    }
}

三、Android系统处理死锁方案

Android系统的Framework层有一个WatchDog用于定期检测关键系统服务是否发生死锁。WatchDog功能主要是分析系统核心服务和重要线程是否处于Blocked状态。

下面我们以Android 9.0为例分析WatchDog的实现原理。通过分析源码,也可以给自己实现一套死锁监控提供一些思路。源码见:WatchDog

看源码之前,可以先自己思考下,如果让我们去实现一个WatchDog,我们会如何设计。其实原理倒是不难,无外乎需要做两件事情。

  1. 定期轮询检测系统中核心的线程的状态
  2. 检测到卡死后,将相关对应的线程,进程及其他软硬件信息输出

其实WatchDog也是这么设计的。WatchDog是继承自Thread,那么我们分析它的工作流程也就从run方法开始吧。

为了方便代码展示,下面源码只保留一些关键代码。run方法是整个检测的核心,我在代码片段里面标注了“代码关键点*”字样,方便在文中引用定位。

    public void run() {
        boolean waitedHalf = false;
        while (true) {//我们要在Android系统运行的整个过程中监控,当然我们需要一个死循环
            final List<HandlerChecker> blockedCheckers;
            final String subject;
            final boolean allowRestart;
            int debuggerWasConnected = 0;
            synchronized (this) {
                long timeout = CHECK_INTERVAL;
               //代码关键点1
                for (int i=0; i<mHandlerCheckers.size(); i++) {
                    HandlerChecker hc = mHandlerCheckers.get(i);
                    hc.scheduleCheckLocked();
                }
                ....
        //代码关键点2
                long start = SystemClock.uptimeMillis();
                while (timeout > 0) {
                ...
                    try {
                        wait(timeout);
                    } catch (InterruptedException e) {
                        Log.wtf(TAG, e);
                    }
                ...
                    timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start);
                }
                ...
        //代码关键点3
                boolean fdLimitTriggered = false;
                if (mOpenFdMonitor != null) {
                    fdLimitTriggered = mOpenFdMonitor.monitor();
                }
        //代码关键点4
                if (!fdLimitTriggered) {
                    final int waitState = evaluateCheckerCompletionLocked();
                    if (waitState == COMPLETED) {
                        waitedHalf = false;
                        continue;
                    } else if (waitState == WAITING) {
                        continue;
                    } else if (waitState == WAITED_HALF) {
                        if (!waitedHalf) {
                            ArrayList<Integer> pids = new ArrayList<Integer>();
                            pids.add(Process.myPid());
                            ActivityManagerService.dumpStackTraces(true, pids, null, null,
                                getInterestingNativePids());
                            waitedHalf = true;
                        }
                        continue;
                    }
                    blockedCheckers = getBlockedCheckersLocked();
                    subject = describeCheckersLocked(blockedCheckers);
                } else {
                    blockedCheckers = Collections.emptyList();
                    subject = "Open FD high water mark reached";
                }
                allowRestart = mAllowRestart;
            }

      //代码关键点5
      //代码运行到这里,说明系统已经卡死
           
            final File stack = ActivityManagerService.dumpStackTraces(
                    !waitedHalf, pids, null, null, getInterestingNativePids());
            doSysRq('w');
            doSysRq('l');
            ... 
            IActivityController controller;
            if (controller != null) {
              
      ...
            int res =controller.systemNotResponding(subject);
            if (res >= 0) {
             ...
               continue;
             } 
            }
      // 代码关键点6
            if (Debug.isDebuggerConnected()) {
                debuggerWasConnected = 2;
            }
            if (debuggerWasConnected >= 2) {
            } else if (debuggerWasConnected > 0) {
            } else if (!allowRestart) {
            } else {//只有这种情况下,杀死system_server
           ...
           //代码关键点6
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
            waitedHalf = false;
        }
    }

整个run方法是一个死循环,这也是可以理解的,毕竟WatchDog需要在Android系统的整个运行期间进行监测。

在“代码关键点1”这里,通过遍历所有需要检测的线程,需要检测的线程集合是在WatchDog的构造函数中初始化的。

    private Watchdog() {
        super("watchdog");
      ... 
        mMonitorChecker = new HandlerChecker(FgThread.getHandler(),
                "foreground thread", DEFAULT_TIMEOUT);
        mHandlerCheckers.add(mMonitorChecker);
        mHandlerCheckers.add(new HandlerChecker(new Handler(Looper.getMainLooper()),
                "main thread", DEFAULT_TIMEOUT));
        mHandlerCheckers.add(new HandlerChecker(UiThread.getHandler(),
                "ui thread", DEFAULT_TIMEOUT));
        mHandlerCheckers.add(new HandlerChecker(IoThread.getHandler(),
                "i/o thread", DEFAULT_TIMEOUT));
        mHandlerCheckers.add(new HandlerChecker(DisplayThread.getHandler(),
                "display thread", DEFAULT_TIMEOUT));
        addMonitor(new BinderThreadMonitor());
        mOpenFdMonitor = OpenFdMonitor.create();//这个monitor有额外作用,后面我们会有提到
      ...
    }

WatchDog构造函数中,初始化了我们要监控的系统线程。包含FgThread,主线程,UiThread,IoThread,DisplayThread,Binder通信线程。(需要着重说明的是监控FgThread的mMonitorChecker通过向外部暴露接口,通过调用WatchDog的addMonitor方法,来监控所有实现了Monitor接口的服务。)

    public void addMonitor(Monitor monitor) {
        ....
            mMonitorChecker.addMonitor(monitor);
        }
    }

代码中的HandlerChecker便是今天的主角之一,它的主要作用就是用来检测线程是否卡死。在“代码关键点1”的循环中,调用了scheduleCheckLocked,而这个方法是HandlerChecker的核心。下面HandlerChecker代码片段,这个方法通过postAtFrontOfQueue向被监控线程的Handler消息队列的头部插入当前HandlerChecker,如果被监控线程消息执行正常,则会回调HandlerChecker的run方法,在run方法里面遍历所有Monitor对象(实现Monitor接口的服务很多,包含AMS,WMS,IMS等),执行monitor方法,如果服务正常,最后我们便会将mCompleted置为true。这个mCompleted变量就是后续WatchDog用来判断对应线程是否卡死依据。

展开阅读全文

本文系作者在时代Java发表,未经许可,不得转载。

如有侵权,请联系nowjava@qq.com删除。

编辑于

关注时代Java

关注时代Java