[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**

從源碼和源碼的解釋可以看出,這個函數是用來獲取當前線程等待的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**

用同樣的方式來獲取這個函數符號地址
~~~
// 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
最后通過互斥條件判斷出死鎖線程,把造成死鎖的線程堆棧信息輸出,如下

檢查出死鎖,線下可以彈窗或者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 性能優化
- 數據跨平臺