移动开发最新面试官:“遇到过死锁问题吗?怎么发生的?如何解决呢?,嵌入式开发面试题及答案详解

最后

我坚信,坚持学习,每天进步一点,滴水穿石,我们离成功都很近!
以下是总结出来的字节经典面试题目,包含:计算机网络,Kotlin,数据结构与算法,Framework源码,微信小程序,NDK音视频开发,计算机网络等。

字节高级Android经典面试题和答案


网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

class SystemMonitor {

private ArrayList playerArrayList;//所有玩家

private ArrayList 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 copy;

synchronized (this){

copy = new ArrayList<>(playerArrayList);

}

for (Player player : copy){

System.out.println(“玩家有”+ player.getCardCount() + “张牌”);

}

}

2.3 线程饥饿死锁

在线程池中,如果任务依赖于其他任务,就可能产生死锁。举一个简单的单线程 Executor 的例子,如果任务 A 已经在 Executor 中运行,而任务 A 又向相同的 Executor 中提交了一个任务 B,通常情况下,这样会产生死锁。

任务 B 在队列中一直等待任务 A 完成,而任务 A 由于是在单线程 Executor 中,所以又在等待任务 B 执行完成,这样就造成了死锁。在更大的线程池中,考虑极限情况,如果所有正在执行任务的线程,都在等待之前提交到线程池中排队的任务,这样线程会永远等待下去,这种问题称为线程饥饿死锁

下面的代码展示了线程饥饿死锁。

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

@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 second = new Callable() {

@Override

public String call() throws Exception {

Thread.sleep(100);

return “second callable”;

}

};

Callable first = new Callable() {

@Override

public String call() throws Exception {

Thread.sleep(10);

Future secondFuture = executor.submit(second);

String secondResult = secondFuture.get();

Thread.sleep(10);

return "first callable. second result = " + secondResult;

}

};

List<Future> futures = new ArrayList<>();

for(int i = 0; i< 5; i++){

System.out.println("submit : " + i);

Future 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() 方法是整个检测的核心,我在代码片段里面标注了「代码关键点 x」字样,方便在文中引用定位。

public void run() {

boolean waitedHalf = false;

while (true) {//我们要在Android系统运行的整个过程中监控,当然我们需要一个死循环

final List 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 pids = new ArrayList();

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

//这个monitor有额外作用,后面我们会有提到

mOpenFdMonitor = OpenFdMonitor.create();

}

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 用来判断对应线程是否卡死依据。

public final class HandlerChecker implements Runnable {

private final Handler mHandler;

private final ArrayList mMonitors = new ArrayList();

private boolean mCompleted;

public void scheduleCheckLocked() {

if (mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling()) {//特殊的条件,需要注意,下面有解释

mCompleted = true;

return;

}

if (!mCompleted) {

return;

}

mCompleted = false;

mCurrentMonitor = null;

mStartTime = SystemClock.uptimeMillis();

mHandler.postAtFrontOfQueue(this);

}

@Override

public void run() {

final int size = mMonitors.size();

for (int i = 0 ; i < size ; i++) {

synchronized (Watchdog.this) {

mCurrentMonitor = mMonitors.get(i);

}

mCurrentMonitor.monitor();

}

synchronized (Watchdog.this) {

mCompleted = true;

mCurrentMonitor = null;

}

}

}

scheduleCheckLocked() 方法中有一个代码引起了我们的注意,如果 mHandler.getLooper().getQueue().isPolling()true,那么直接将 mCompleted 置为 true,这又是什么原理?

通过查阅 MessageQueue 源码,里面的一段注释解决了我们的迷惑。

Returns whether this looper’s thread is currently polling for more work .This is a good signal that the loop is still alive rather than being stuck handling a callback

这段话含义就是 isPolling 表示正在从队列中取消息,为 true 则代表 Looper 依然运行良好,通过这个标记就不需要等待回调来得知状态,这样效率更高。

了解了检测卡死的原理,那我们继续回到 WatchDog 的 run() 方法,来看「代码关键点 2」。通过 wait() 方法实现了每 30s 检测一次的效果,这里看到了 Google 工程师的一个小技巧,由于 wait()timeout 时间可能没那么准确,为了保证至少等待 30s,使用了一个 while 循环,并且循环完毕通过 timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start); 来保证时间够 30s。

「代码关键点 3」中使用了 OpenFdMonitor,这个类的主要作用是为了判断剩余可用文件句柄的数量,大家知道 Linux 中打开文件都需要分配文件句柄,系统的文件句柄数量是有限制的。

当然这个 OpenFdMonitor 只在编译模式为 userdebug 和 eng 的 Android 编译版本起作用,这也是为了方便开发人员调试信息。

「代码关键点 4」中 evaluateCheckerCompletionLocked() 便是用来评估当前所有线程的卡死情况。

private int evaluateCheckerCompletionLocked() {

int state = COMPLETED;

for (int i=0; i<mHandlerCheckers.size(); i++) {

HandlerChecker hc = mHandlerCheckers.get(i);

state = Math.max(state, hc.getCompletionStateLocked());

}

return state;

}

代码获取了当前线程中状态值最大的 state。

state 的定义如下:

  • COMPLETED = 0; 已完成,不存在卡死情况;

  • WAITING = 1; 等待时间小于 DEFAULT_TIMEOUT 的一半,即 < 30s;

  • WAITED_HALF = 2; 等待时间超过 DEFAULT_TIMEOUT 的一半,即 >=30s;

  • OVERDUE = 3; 等待时间大于等于 DEFAULT_TIMEOUT ,即 >=60s;

如果有线程状态已经是 OVERDUE,那么说明被监控的线程有卡死情况。我们的流程也来到了「代码关键点 5」。这里就比较好理解了,通过 dumpStackTraces 输出 kernel 栈信息,通过 doSysRq 触发系统 dump 所有阻塞线程堆栈。这样所有相关的信息就保存好了。

「代码关键点 6」中,以下几种情况,即使触发了 WatchDog,也不杀死系统进程。

  • debuggerWasConnected>=0 debuggerWasConnected>=2 代表 debugger 正在连接调试中

  • allowRestart 设置为 true,是通过 adb logcat am hang 命令设置的

最后通过下面两行代码将 SystemServer 进程杀死,当 system_server 被杀后,就会导致 Zygote 进程自杀,进而做到 Zygote 进程的重启。而这个现象也就是我们平常看到了手机死机了,然后又自动重启的现象。

Process.killProcess(Process.myPid());

System.exit(10);

四、Android 开发过程中死锁分析方法


分析完系统如何处理死锁情况后,我们再来看看在 Android 开发中最容易碰到的死锁表现形式 ANR。

当然产生 ANR 的原因很多,死锁只是其中一种。如果 ANR 发生,对应的应用会收到 SIGQUIT 异常终止信号,dalvik 虚拟机就会自动在 /data/anr/ 目录下生成 trace.txt(Android8.1 以后文件名不是这个了)文件,这个文件记录了在发生 ANR 时刻系统各个线程的执行状态,trace 文件中记录的线程执行状态详细描述了各个线程加锁等待的情况。

通过分析,就可以相对容易的找到发生死锁所在的线程及代码。

主线程死锁导致的问题,可以通过 ANR 的 trace 文件分析,如果是非主线程呢,这种死锁一般很难察觉,但是这种死锁有时候也会造成很严重的后果,因为线程可能一直在占用某些资源,比如端口,数据库连接,文件句柄等。对于普通的 java 程序,JVM 提供了 jstack 工具,可以将线程信息 dump 出来进行分析。

由于 Android 系统中没有提供类似 jstack 的工具,

学习路线+知识梳理

花了很长时间,就为了整理这张详细的知识路线脑图。当然由于时间有限、能力也都有限,毕竟嵌入式全体系实在太庞大了,包括我那做嵌入式的同学,也不可能什么都懂,有些东西可能没覆盖到,不足之处,还希望小伙伴们一起交流补充,一起完善进步。

这次就分享到这里吧,下篇见

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

的应用会收到 SIGQUIT 异常终止信号,dalvik 虚拟机就会自动在 /data/anr/ 目录下生成 trace.txt(Android8.1 以后文件名不是这个了)文件,这个文件记录了在发生 ANR 时刻系统各个线程的执行状态,trace 文件中记录的线程执行状态详细描述了各个线程加锁等待的情况。

通过分析,就可以相对容易的找到发生死锁所在的线程及代码。

主线程死锁导致的问题,可以通过 ANR 的 trace 文件分析,如果是非主线程呢,这种死锁一般很难察觉,但是这种死锁有时候也会造成很严重的后果,因为线程可能一直在占用某些资源,比如端口,数据库连接,文件句柄等。对于普通的 java 程序,JVM 提供了 jstack 工具,可以将线程信息 dump 出来进行分析。

由于 Android 系统中没有提供类似 jstack 的工具,

学习路线+知识梳理

花了很长时间,就为了整理这张详细的知识路线脑图。当然由于时间有限、能力也都有限,毕竟嵌入式全体系实在太庞大了,包括我那做嵌入式的同学,也不可能什么都懂,有些东西可能没覆盖到,不足之处,还希望小伙伴们一起交流补充,一起完善进步。

这次就分享到这里吧,下篇见

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值