[TOC]
# 死锁的四大条件
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
# 死锁的监测 方案1 (利用生成系统的traces文件)
定期打印全部现场堆栈
对HandleThread的监控(类似Looper 装炸弹/拆炸弹/引爆)
## 死锁分析
https://blog.csdn.net/Tencent\_Bugly/article/details/79757867
Android在发生ANR时有一套系统机制:
1、Android应用发生ANR时,系统会发出SIGQUIT信号给发生ANR进程。
2、系统信号捕捉线程触发输出/data/anr/traces.txt文件,记录问题产生虚拟机、线程堆栈相关信息。
3、这个trace文件中包含了线程信息和锁的信息,借助这个trace文件可以分析卡死的原因。
由此,如果利用这个系统原有的机制,自己在线程卡死时候触发traces文件的形成进行上报,便可以把线程卡死的关键进行进行上报。本监控方案便是利用系统机制进行卡死信息的抓取:
1、当监控线程发现被监控线程卡死时,主动向系统发送SIGQUIT信号。
2、等待/data/anr/traces.txt文件生成。
3、文件生成以后进行上报。
# 死锁监控 方案2
在发生ANR的时候,有时候只有主线程堆栈信息可能还不够,例如发生死锁的情况,**需要知道当前线程在等待哪个锁,以及这个锁被哪个线程持有**,然后把发生死锁的线程堆栈信息都收集到。
流程如下:
1. 获取当前blocked状态的线程
2. 获取该线程想要竞争的锁
3. 获取该锁被哪个线程持有
4. 通过关系链,判断死锁的线程,输出堆栈信息
在Java层并没有相关API可以实现死锁监控,可以从Native层入手。
## 获取当前blocked状态的线程
这个比较简单,一个for循环就搞定,不过我们要的线程id是native层的线程id,Thread 内部有一个native线程地址的字段叫 `nativePeer`,通过反射可以获取到。
~~~
Thread[] threads = getAllThreads();
for (Thread thread : threads) {
if (thread.getState() == Thread.State.BLOCKED) {
long threadAddress = (long) ReflectUtil.getField(thread, "nativePeer");
// 找不到地址,或者线程已经挂了,此时获取到的可能是0和-1
if (threadAddress <= 0) {
continue;
}
...后续
}
}
~~~
有了native层线程地址,还需要找到native层相关函数
## 获取当前线程想要竞争的锁
从ART 源码可以找到这个函数 [androidxref.com/8.0.0\_r4/xr…](https://link.juejin.cn?target=http%3A%2F%2Fandroidxref.com%2F8.0.0_r4%2Fxref%2Fart%2Fruntime%2Fmonitor.cc "http://androidxref.com/8.0.0_r4/xref/art/runtime/monitor.cc")
函数:**Monitor::GetContendedMonitor**
![](https://img.kancloud.cn/04/29/0429ae3a73c0bff44f8ba6d84a867ced_1240x586.png)
从源码和源码的解释可以看出,这个函数是用来获取当前线程等待的Monitor。
顺便说说Monitor以及Java对象结构
#### Monitor
**Monitor是一种并发控制机制**,提供多线程环境下的互斥和同步,以支持安全的并发访问。
Monitor由以下3个元素组成:
1. 临界区:例如synchronize修饰的代码块
2. 条件变量:用来维护因不满足条件而阻塞的线程队列
3. Monitor对象,维护Monitor的入口、临界区互斥量(即锁)、临界区和条件变量,以及条件变量上的阻塞和唤醒
感兴趣可以参考这一篇文章 [说一说管程(Monitor)及其在Java synchronized机制中的体现](https://link.juejin.cn?target=https%3A%2F%2Fwww.jianshu.com%2Fp%2Fe624460c645c "https://www.jianshu.com/p/e624460c645c")
#### Java的Class对象
Java的Class对象包括三部分组成:
1. 对象头:MarkWord和对象指针
> **MarkWord(标记字段)**:保存哈希码、分代年龄、**锁标志位**、偏向线程ID、偏向时间戳等信息 **对象指针**:即指向当前对象的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2. 实例数据:对象实际的数据
3. 对齐填充:按8字节对齐(JVM自动内存管理系统要求对象起始地址必须是8字节的整数倍)。例如Integer对象,对象头MarkWord和对象指针分别占用4字节,实例数据4字节,那么对齐填充就是4字节,Integer占用内存是int的4倍。
* * *
回到 `GetContendedMonitor` 函数,我们可以通过打开动态库`libart.so`,然后使用`dlsym`获取函数的符号地址,然后就可以进行调用了。
由于Android 7.0开始,系统限制App中调用`dlopen`,`dlsym`等函数打开系统动态库,我们可以使用 [ndk\_dlopen](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Frrrfff%2Fndk_dlopen "https://github.com/rrrfff/ndk_dlopen")这个库来绕过这个限制
~~~
//1、初始化
ndk_init(env);
//2、打开动态库libart.so
void *so_addr = ndk_dlopen("libart.so", RTLD_NOLOAD);
if (so_addr == NULL) {
return 1;
}
~~~
打开动态库之后,会返回动态库的内存地址,接下来就可以通过`dlsym`获取`GetContendedMonitor`这个函数的符号地址,只不过要注意,c++可以重载,所以它的函数符号比较特殊,需要从`libart.so`中搜索匹配找到
~~~
//c++跟c不一样,c++可以重载,描述符会变,需要打开libart.so,在里面搜索查找GetContendedMonitor的函数符号
//http://androidxref.com/8.0.0_r4/xref/system/core/libbacktrace/testdata/arm/libart.so
//获取Monitor::GetContendedMonitor函数符号地址
get_contended_monitor = ndk_dlsym(so_addr, "_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE");
if (get_contended_monitor == NULL) {
return 2;
}
复制代码
~~~
到此,第一个函数的符号地址找到了,接下来要找另外一个函数
## 获取目标锁被哪个线程持有
**函数:Monitor::GetLockOwnerThreadId**
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/668ca9bf80d642169a6bce7cf3c45ed7~tplv-k3u1fbpfcp-watermark.awebp)
用同样的方式来获取这个函数符号地址
~~~
// Monitor::GetLockOwnerThreadId
//这个函数是用来获取 Monitor的持有者,会返回线程id
get_lock_owner_thread = ndk_dlsym(so_addr, get_lock_owner_symbol_name(api_level));
if (get_lock_owner_thread == NULL) {
return 3;
}
复制代码
~~~
由于从android 10开始,这个`GetLockOwnerThreadId`函数符号有变化,所以需要通过api版本来判断使用哪一个
~~~
const char *get_lock_owner_symbol_name(jint level) {
if (level <= 29) {
//android 9.0 之前
//http://androidxref.com/9.0.0_r3/xref/system/core/libbacktrace/testdata/arm/libart.so 搜索 GetLockOwnerThreadId
return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE";
} else {
//android 10.0
// todo 10.0 源码中这个函数符号变了,需要自行查阅
return "_ZN3art7Monitor20GetLockOwnerThreadIdEPNS_6mirror6ObjectE";
}
}
~~~
到此,就得到了两个函数符号地址,接下来就把blocked状态的native线程id传过去,调用就行了
## 找到一直不释放锁的线程
~~~
Java_com_lanshifu_demo_anrmonitor_DeadLockMonitor_getContentThreadIdArt(JNIEnv *env,jobject thiz,jlong native_thread) {
LOGI("getContentThreadIdArt");
int monitor_thread_id = 0;
if (get_contended_monitor != NULL && get_lock_owner_thread != NULL) {
LOGI("get_contended_monitor != NULL");
//1、调用一下获取monitor的函数,返回当前线程想要竞争的monitor
int monitorObj = ((int (*)(long)) get_contended_monitor)(native_thread);
if (monitorObj != 0) {
LOGI("monitorObj != 0");
// 2、获取这个monitor被哪个线程持有,返回该线程id
monitor_thread_id = ((int (*)(int)) get_lock_owner_thread)(monitorObj);
} else {
LOGE("GetContendedMonitor return null");
monitor_thread_id = 0;
}
} else {
LOGE("get_contended_monitor == NULL || get_lock_owner_thread == NULL");
}
return monitor_thread_id;
}
~~~
两个步骤:
1. 获取当前线程要竞争的锁
2. 获取这个锁被哪个线程持有
通过两个步骤,得到的是那个一直不释放锁的线程id。
## 通过算法,找到死锁
前面已经知道当前blocked状态的线程id(还需要转换成native线程id),以及这个blocked线程在等待哪个线程释放锁,也就是得到关系链:
1. A等待B B等待A
2. A等待B B等待C C等待A ...
3. 其它...
如何判断有死锁?我们可以用Map来保存对应关系
map\[A\]=B
map\[B\]=A
最后通过互斥条件判断出死锁线程,把造成死锁的线程堆栈信息输出,如下
![](https://img.kancloud.cn/52/bd/52bd06c8b3aaf59ecddbe6c4b703e6b9_1240x176.png)
检查出死锁,线下可以弹窗或者toast,线上则可以采集数据上报。
## 死锁监控小结
死锁监控原理还是比较清晰的:
1. 获取blocked状态的线程
2. 获取该线程想要竞争的锁(native层函数)
3. 获取这个锁被哪个线程持有(native层函数)
4. 有了关系链,就可以找出造成死锁的线程
由于死锁监控涉及到native层代码,对于很多应用层开发的同学来说可能有点难度,
# 参考资料
[《手Q Android线程死锁监控与自动化分析实践》](https://blog.csdn.net/Tencent_Bugly/article/details/79757867)
[卡顿、ANR、死锁,线上如何监控](https://juejin.cn/post/6973564044351373326)
- Android
- 四大组件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介绍
- MessageQueue详细
- 启动流程
- 系统启动流程
- 应用启动流程
- Activity启动流程
- View
- view绘制
- view事件传递
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大数据
- Binder小结
- Android组件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 迁移与修复
- Sqlite内核
- Sqlite优化v2
- sqlite索引
- sqlite之wal
- sqlite之锁机制
- 网络
- 基础
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP进化图
- HTTP小结
- 实践
- 网络优化
- Json
- ProtoBuffer
- 断点续传
- 性能
- 卡顿
- 卡顿监控
- ANR
- ANR监控
- 内存
- 内存问题与优化
- 图片内存优化
- 线下内存监控
- 线上内存监控
- 启动优化
- 死锁监控
- 崩溃监控
- 包体积优化
- UI渲染优化
- UI常规优化
- I/O监控
- 电量监控
- 第三方框架
- 网络框架
- Volley
- Okhttp
- 网络框架n问
- OkHttp原理N问
- 设计模式
- EventBus
- Rxjava
- 图片
- ImageWoker
- Gilde的优化
- APT
- 依赖注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 协程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 运行期Java-hook技术
- 编译期hook
- ASM
- Transform增量编译
- 运行期Native-hook技术
- 热修复
- 插件化
- AAB
- Shadow
- 虚拟机
- 其他
- UI自动化
- JavaParser
- Android Line
- 编译
- 疑难杂症
- Android11滑动异常
- 方案
- 工业化
- 模块化
- 隐私合规
- 动态化
- 项目管理
- 业务启动优化
- 业务架构设计
- 性能优化case
- 性能优化-排查思路
- 性能优化-现有方案
- 登录
- 搜索
- C++
- NDK入门
- 跨平台
- H5
- Flutter
- Flutter 性能优化
- 数据跨平台