死锁是一种线程同步问题,指两个或多个线程在执行过程中由于相互等待对方释放资源,导致它们永远无法继续执行下去的现象。

一般死锁满足下面四个条件:

  1. 互斥条件:一个资源每次只能被一个线程占用。

  2. 持有并等待条件:一个线程已经持有至少一个资源,同时又请求新的资源,但该资源已被其他线程占用。

  3. 不可剥夺条件:线程已获得的资源在未使用完成之前,不能被其他线程强行剥夺。

  4. 循环等待条件:存在一个线程等待链,链中每个线程都在等待下一个线程所持有的资源。

实例

由于涉及公司的代码包名,我进行了替换,其他都是真实信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
----- pid 2298 at 2024-08-28 21:09:32.901621896+0800 -----
Cmd line: com.android.launcher
Build fingerprint: 'xxx:user/release-keys'
ABI: 'arm64'
Build type: optimized
suspend all histogram: Sum: 1.030s 99% C.I. 5.214us-4429.937us Avg: 139.048us Max: 49675us
DALVIK THREADS (60):
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x731e4920 self=0xb40000716ca3bc00
| sysTid=2298 nice=0 cgrp=foreground sched=1073741824/0 handle=0x721ae71d20
| state=S schedstat=( 7416519353172 1278618376091 9259320 ) utm=674457 stm=67194 core=7 HZ=100
| stack=0x7fd9a08000-0x7fd9a0a000 stackSize=8188KB
| held mutexes=
at x7.e.b(unavailable:0)
- waiting to lock <0x0bef5c87> (a w7.a) held by thread 13
at J6.b.onTransact(unavailable:214)
at android.os.Binder.execTransactInternal(Binder.java:1505)
at android.os.Binder.execTransact(Binder.java:1444)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(BinderProxy.java:586)
at x7.e.f(unavailable:59)
at x7.d.onServiceConnected(unavailable:35)
at x7.a.onServiceConnected(unavailable:29)
- locked <0x0a85b7b4> (a java.util.Collections$SynchronizedSet)
at android.app.LoadedApk$ServiceDispatcher.doConnected(LoadedApk.java:2256)
at android.app.LoadedApk$ServiceDispatcher$RunConnection.run(LoadedApk.java:2289)
at android.os.Handler.handleCallback(Handler.java:959)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loopOnce(Looper.java:242)
at android.os.Looper.loop(Looper.java:327)
at android.app.ActivityThread.main(ActivityThread.java:8843)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:617)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1002)
DumpLatencyMs: 87.2616

"BI_Common_SDK_workThread" prio=5 tid=13 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x1403cfb0 self=0xb400007131cd6400
| sysTid=2337 nice=0 cgrp=foreground sched=1073741824/0 handle=0x70d07b9770
| state=S schedstat=( 1894403086 2330218128 4644 ) utm=70 stm=118 core=5 HZ=100
| stack=0x70d06b6000-0x70d06b8000 stackSize=1037KB
| held mutexes=
at x7.c.O(unavailable:47)
- waiting to lock <0x0a85b7b4> (a java.util.Collections$SynchronizedSet) held by thread 1
- locked <@addr=0x141ff608> (a x7.c)
at x7.e.g(unavailable:26)
at x7.e.a(unavailable:16)
- locked <0x0bef5c87> (a w7.a)
at androidx.compose.ui.platform.b.l(unavailable:88)
- locked <@addr=0x141fe7d0> (a y7.f)
at com.android.example.reportData(unavailable:42)
at com.android.example.reportDataFrom(unavailable:31)
atcom.android.example.access$1400(unavailable:0)
at com.android.example$3.run(unavailable:74)
at android.os.Handler.handleCallback(Handler.java:959)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loopOnce(Looper.java:242)
at android.os.Looper.loop(Looper.java:327)
at android.os.HandlerThread.run(HandlerThread.java:85)
DumpLatencyMs: 166.269

从第8行可知,状态为Blocked,在之前介绍trace信息的时候介绍过各个状态的含义,这里不贴了,Blocked就是线程阻塞,等待获取对象锁;

第15行:- waiting to lock <0x0bef5c87> (a w7.a) held by thread 13,这里可以看出等待标识是0x0bef5c87的锁,这个锁被线程13持有;

第24行:- locked <0x0a85b7b4> (a java.util.Collections$SynchronizedSet),这里持有的是标识为0x0a85b7b4的锁;

通过第15行的分析标识为0x0bef5c87被线程13持有,那么需要找线程13的锁,从整个trace信息中我们过滤到了tid=13(37行)的信息,显示状态也为Blocked,说明这个线程也被阻塞了,那么继续分析;

第44行:- waiting to lock <0x0a85b7b4> (a java.util.Collections$SynchronizedSet) held by thread 1,这里是等待标识为0x0a85b7b4的锁,这个正是24行中的锁,后面显示被线程1所持有,那么正好是tid=1,与前面对应上了;

第48行: - locked <0x0bef5c87> (a w7.a),线程13持有的标识为0x0bef5c87的锁,正好是线程1等待的锁;

因此,这里是一个死锁,导致了ANR的现象,那么根据上面信息可以在51行找到调用处,分析该方法对应的代码,就可以解决了。

Android中常见的死锁场景:

  1. 多线程的嵌套锁定:当两个线程试图以不同顺序锁定资源时,就会导致死锁。
  2. 主线程阻塞:Android 中主线程(UI 线程)被用于更新界面。如果在主线程中使用了同步锁(如 synchronized),而该锁被另一个工作线程持有,同时工作线程也需要主线程完成某些任务,可能导致死锁。
  3. 跨线程通信死锁:例如,一个线程等待 Handler 发送的消息,而 Handler 本身在另一个线程中被阻塞。

如何避免死锁:

避免嵌套锁定

  • 避免在一个锁内请求另一个锁,简化锁的获取逻辑。
  • 如果必须嵌套锁定,应确保所有线程以相同的顺序获取锁。

减少锁的持有时间

  • 尽可能缩小锁的作用范围,避免长时间持有锁。

使用更高级的并发工具

  • 例如,使用 ReentrantLock 或其他非阻塞的并发工具,它们提供了更灵活的锁机制,如超时尝试加锁。

主线程避免长时间锁定

  • 避免在主线程中执行耗时操作,尤其是加锁操作。耗时任务应交由后台线程处理。

分析和监控

  • 使用工具如 StrictMode 或 Android Studio Profiler 分析线程和锁的使用情况,发现潜在的死锁问题。

尽量减少共享资源的使用

  • 如果可以,通过无共享(如消息传递或回调)的方式来避免死锁的根本问题。