去年处理一些性能问题的时候,遇到过一些主线程等待锁的问题,如果主线程等待锁的时间太长,就会出现主线程卡顿甚至ANR。所以我们需要通过技术手段去检测可能存在的锁等待。
技术基础
如何检测主线程等待锁?这里涉及一些技术基础知识点:
线程状态
Java线程可以通过 getState
获取线程的状态,线程状态包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITTING、TERMINATED 几个状态,其中 BLOCKED 就是等待锁的时候进入的阻塞状态。
获取线程想要竞争的Monitor
在art源码的 monitor.cc 里面通过 GetContendedMonitor 函数可以获取某个 Thread 在等待的 Monitor:
获取Monitor被持有的线程
也是在 monitor.cc 里面可以通过 GetLockOwnerThreadId 获取 monitor 被持有的线程:
实现思路
有了上述技术基础,我们就可以出现一个大致的主线程锁等待的监控。我们可以按一定频次轮训主线程的状态,如果主线程处于blocked状态,那么说明主线程和其他线程在竞争锁。通过上述的2个jni函数,就可以得到被等待的线程的id。
具体实现和问题
在具体实现的时候我们需要处理一些问题。这部分我们来一一介绍。
调用jni函数
第一个问题是如何调用 Monitor::GetContendedMonitor 和 Monitor::GetLockOwnerThreadId。 在c/c++中,我们可以通过 dlopen打开一个so文件,通过dlsym获取函数的地址。但是dlopen和dlsym在Android中被限制调用。我们可以使用 ndk_dlopen 这个库去调用。这个库的关键实现在 github.com/Rprop/ndk_d… 里,他会插入一段汇编代码,模拟系统调用去调用dlopen和dlsym。 这块其实我也不是很明白,只能从issus里找到一个大概的原理解释。不过没关系,他的代码很少,我们直接copy使用即可。 这里还有个问题就是GetContendedMonitor函数的参数是一个jni的Thread指针。我们如何传入Thread指针呢?索性Java的Thread对象里面保存了jni层的对象指针地址,我们通过反射就可以获取Thread指针。 获取到函数签名之后,通过ndk_dlopen调用即可获取线程等待的锁的持有线程的id:
对齐线程id
上面的代码我们获取了持有锁的线程的id,但是实际上这个id和java层的线程id并不是同一个线程id。 jni层获取的线程id调用了 Monitor::GetOwnerThreadId: 这里取的是tls32_里的thin_lock_thread_id: 而Jave层的线程id则是Java层自己维护的: 创建线程的时候会调用 nextThreadID 复制给 tid,这只是一个全局的数量自增一下。 那么我们怎么通过jni返回的线程id匹配到java的Thread对象呢?我们可以从c层的Thread对象的对象布局入手。通过分析Thread类的代码,去掉函数、去掉static字段后,代码如下:
class EXPORT Thread {
struct alignas(4) tls_32bit_sized_values {
Atomic<uint32_t> state_and_flags;
uint32_t thin_lock_thread_id;
uint32_t tid;
const bool32_t daemon;
bool32_t throwing_OutOfMemoryError;
//...
} tls32_;
}
这里刚好 tls32_ 是 Thread 对象第一个定义的字段,所以tls32_就是Thread指针指向内存的开头,而 tls32_里面定义的对象顺序是 state_and_flags、thin_lock_thread_id、tid...., 所以Thread指针指向内存再+2,就是指向的 thin_lock_thread_id。 我们传入Java Thread 的 nativePeer,再加上2,就是thin_lock_thread_id 的地址,所以我们可以通过Java Thread算出他的 thin_lock_thread_id,和返回的持有锁线程id做对比,如果对比上了,这个Java Thread对象就是持有锁线程,我们就可以获取他的调研堆栈。
总结
到这里我们基本把主线程锁等待监控的方案思路和关键技术点过了一遍,通过上述方法,我们就能实现我们自己的主线程锁监控。