线程监控 - 死锁、存活周期与 CPU 占用率

写在前面:

大家学习知识不用死抓怎么实现,很多同学认为学了套路能做到举一反三就不错了,这其实还是停留在“术”的层面。大家要学会了解底层的原理自己去折腾,所以这也是为什么我们要花将近一年左右的时间,去学 NDK 去学 Linux 内核,因为很多东西网上也是搜索不到的。

监控死锁:

主线程死锁容易 ANR ,其他线程死锁容易引起异常(不是闪退但会引起用户杀死或卸载 App)。开发需求的时候我们其实很少会自己写出死锁( sdk 开发的除外) 很多情况下都是不小心调用了第三方的或者系统的一些 API 导致的。那我们有没有办法把线上死锁引起的 ANR 上报到服务器呢?或者说有没有什么方法可以及时的监控到死锁?先来看一个死锁的例子

       Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (deadLock1) {
                    try {
                        sleep_(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    synchronized (deadLock2) {
                        Log.e("TAG","thread1");
                    }
                }
            }
        }, "testThread1");

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (deadLock2) {
                    try {
                        sleep_(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    synchronized (deadLock1) {
                        Log.e("TAG","thread2");
                    }
                }
            }
        }, "testThread2");

这是一个比较典型的死锁例子,很多同学肉眼一般能看出来,但是到了线上我们就得做个自动分析,首先如果在本地排查,我们最好的方法是先 dump 到线程的信息

"testThread1@5890" prio=5 tid=0x5210 nid=NA waiting for monitor entry
  java.lang.Thread.State: BLOCKED
     waiting for testThread2@5889 to release lock on <0x1709> (a java.lang.Object)
      at com.darren.optimize.day13.MainActivity$3.run(MainActivity.java:195)
      - locked <0x1708> (a java.lang.Object)
      at java.lang.Thread.run(Thread.java:784)

"testThread2@5889" prio=5 tid=0x5211 nid=NA waiting for monitor entry
  java.lang.Thread.State: BLOCKED
     waiting for testThread1@5890 to release lock on <0x1708> (a java.lang.Object)
      at com.darren.optimize.day13.MainActivity$4.run(MainActivity.java:212)
      - locked <0x1709> (a java.lang.Object)
      at java.lang.Thread.run(Thread.java:784)

如果我们能拿到线程在等待哪个锁释放,当前持有哪个锁这两个信息的话,那么一切就能迎刃而解了。上期有说的在 java 层是无法做到的,但是我们分析了线程创建的底层原理后在 Native 层找到了答案:

http://androidxref.com/9.0.0_r3/xref/art/runtime/monitor.cc

// 当前线程在竞争哪个锁
mirror::Object* Monitor::GetContendedMonitor(Thread* thread) {
    // This is used to implement JDWP's ThreadReference.CurrentContendedMonitor, and has a bizarre
    // definition of contended that includes a monitor a thread is trying to enter...
    mirror::Object* result = thread->GetMonitorEnterObject();
    if (result == nullptr) {
        // ...but also a monitor that the thread is waiting on.
        MutexLock mu(Thread::Current(), *thread->GetWaitMutex());
        Monitor* monitor = thread->GetWaitMonitor();
        if (monitor != nullptr) {
            result = monitor->GetObject();
        }
    }
    return result;
}

// 当前锁被哪个线程持有
uint32_t Monitor::GetLockOwnerThreadId(mirror::Object* obj) {
  DCHECK(obj != nullptr);
  LockWord lock_word = obj->GetLockWord(true);
  switch (lock_word.GetState()) {
    case LockWord::kHashCode:
      // Fall-through.
    case LockWord::kUnlocked:
      return ThreadList::kInvalidThreadId;
    case LockWord::kThinLocked:
      return lock_word.ThinLockOwner();
    case LockWord::kFatLocked: {
      Monitor* mon = lock_word.FatLockMonitor();
      return mon->GetOwnerThreadId();
    }
    default: {
      LOG(FATAL) << "Unreachable";
      UNREACHABLE();
    }
  }
}

有了这两个方法,代码实现起来就比较简单了:

  • 获取所有的线程,判断是不是 BOLCKED 状态
  • 调用 GetContendedMonitor 与 GetLockOwnerThreadId 获取到被锁住的线程
  • 对死锁进行分组,输出死锁对应的位置
// 初始化
extern "C"
JNIEXPORT jint JNICALL
Java_com_darren_optimize_day13_NativeThreadMonitor_nativeInit(JNIEnv *env, jclass clazz, jint level) {
    api_level = level;
    // dlopen libart.so
    void *so_addr = ndk_dlopen("libart.so", RTLD_LAZY);
    if (so_addr == NULL) {
        return 1;
    }
    // Monitor::GetContendedMonitor
    get_contended_monitor = ndk_dlsym(so_addr, "_ZN3art7Monitor19GetContendedMonitorEPNS_6ThreadE");
    if (get_contended_monitor == NULL) {
        return 2;
    }
    // Monitor::GetLockOwnerThreadId
    get_lock_owner_thread = ndk_dlsym(so_addr, get_lock_owner_symbol_name(api_level));
    if (get_lock_owner_thread == NULL) {
        return 3;
    }
    return 0;
}

// 获取当前线程锁被哪个线程持有了
extern "C"
JNIEXPORT jint JNICALL
Java_com_darren_optimize_day13_NativeThreadMonitor_getContentThreadIdArt(JNIEnv *env, jclass clazz,
                                                                         jlong native_thread) {
    int monitor_thread_id = 0;
    if (get_contended_monitor != nullptr && get_lock_owner_thread != nullptr) {
        int monitorObj = ((int (*)(long)) get_contended_monitor)(native_thread);
        if (monitorObj != 0) {
            monitor_thread_id = ((int (*)(int)) get_lock_owner_thread)(monitorObj);
        } else {
            LOGD("GetContendedMonitor return null");
            monitor_thread_id = 0;
        }
    }
    return monitor_thread_id;
}

// 获取线程 id
extern "C"
JNIEXPORT jint JNICALL
Java_com_darren_optimize_day13_NativeThreadMonitor_getThreadIdFromThreadPtr(JNIEnv *env, jclass clazz,
                                                                            jlong nativeThread) {
    if (nativeThread != 0) {
        if (api_level > 20) {  // 大于5.0系统
            int *pInt = reinterpret_cast<int *>(nativeThread);
            pInt = pInt + 3;
            return *pInt;  // 返回 monitor 所使用的Thread id
        }
    } else {
        LOGE("suspendThreadArt failed");
    }
    return 0;
}

NativeThreadMonitor.nativeInit(Build.VERSION.SDK_INT);
Set<Thread> threads = NativeThreadMonitor.getAllThreads();
for (Thread thread : threads) {
  if (thread.getState() == Thread.State.BLOCKED) {
    long threadAddress = (long) ReflectUtil.getFieldObject(thread, "nativePeer");
    // 这里记一下,找不到地址,或者线程已经挂了,此时获取到的可能是0和-1
    if (threadAddress <= 0) {
      continue;
    }
    int blockThreadId = NativeThreadMonitor.getContentThreadIdArt(threadAddress);
    int curThreadId = NativeThreadMonitor.getThreadIdFromThreadPtr(threadAddress);
    if (blockThreadId != 0 && curThreadId != 0) {
      deadLock.put(curThreadId, new DeadLockThread(curThreadId, blockThreadId, thread));
    }
  }
}

try {
  // 将所有情况进行分组
  ArrayList<HashMap<Integer, Thread>> deadLockThreadGroup = deadLockThreadGroup();
  // 再来找死锁
  JSONObject objectGroup = new JSONObject();
  for (int i = 0; i < deadLockThreadGroup.size(); i++) {
    // 所有的组拿出来
    HashMap<Integer, Thread> group = deadLockThreadGroup.get(i);
    JSONArray array = new JSONArray();
    for (int curId : group.keySet()) {
      // 获取 DeadLockThread
      DeadLockThread deadLockThread = deadLock.get(curId);
      if (deadLockThread == null) {
        continue;
      }
      // 获取等待线程
      Thread waitThread = group.get(deadLockThread.blockId);
      if (waitThread == null) {
        continue;
      }
      Thread deadThread = group.get(curId);
      JSONObject temp = new JSONObject();
      JSONArray stacks = new JSONArray();
      temp.put("thread_name", deadThread.getName());
      temp.put("thread_id", deadThread.getId());
      temp.put("wait_thread", waitThread.getName());
      temp.put("wait_id", waitThread.getId());
      StackTraceElement[] stackTraceElements = deadThread.getStackTrace();
      for (StackTraceElement stackTraceElement : stackTraceElements) {
        stacks.put(stackTraceElement.toString());
      }
      temp.put("thread_stack", stacks);
      array.put(temp);
    }
    objectGroup.put("dead_lock_group_" + i, array);
  }
  Log.e("TAG", objectGroup.toString());
} catch (Exception e) {
    e.printStackTrace();
}

监控存活周期:

有些场景下我们想监控线程的存活周期,也就是说线程从开始启动到运行结束总共存活了多长时间,占了多少内存,占了多少 CPU 等等,异常的情况下我们线下要给出警告线上要上报到服务器。目前我们能想到两种方案一种是采用之前讲的 ASM 插桩的方式,但是这种方案很多场景不适用;还有一种是今天要讲到的 Native 插桩。插桩点依旧是之前的线程创建的底层原理:

http://androidxref.com/9.0.0_r3/xref/art/runtime/thread.cc
// 最终想监控这个方法
void* Thread::CreateCallback(void* arg) {
    // ...
}

void *(*old_create_call_back)(void *) = NULL;

void *create_call_back(void *args) {
    // 记录开始时间
    long startTime = time(NULL);
    // 调用原始方法
    void *result = old_create_call_back(args);
    // 获取当前线程信息,计算输出存活时间
    int tid = gettid();
    const char *thread_name = getThreadName(gettid());
    long alive_time = time(NULL) - startTime;
    LOGE("线程信息:thread_id = %d, thread_name = %s, alive_time = %lds", tid, thread_name, alive_time);
    // 获取内存占用,获取 cpu 占用率,异常情况输出警告
    return result;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_darren_optimize_day13_NativeThreadMonitor_monitoringThread(JNIEnv *env, jclass clazz) {
    void *so_addr = ndk_dlopen("libart.so", RTLD_LAZY);
    void *thread_create_call_back = ndk_dlsym(so_addr, "_ZN3art6Thread14CreateCallbackEPv");
    if (registerInlineHook((uint32_t) thread_create_call_back, (uint32_t) create_call_back,
                           (uint32_t **) &old_create_call_back) != ELE7EN_OK) {
        LOGE("monitoringThread registerInlineHook error");
    } else {
        LOGE("monitoringThread registerInlineHook ok");
    }
    if (inlineHook((uint32_t) thread_create_call_back) != ELE7EN_OK) {
        LOGE("monitoringThread inlineHook error");
    } else {
        LOGE("monitoringThread inlineHook ok");
    }
}

监控 CPU 占用率:

cpu 占用率比较简单,我们只需要解析到 /proc/pid/task/tid/stat 与 /proc/pid/stat 即可。

// 进程 stat 信息
extern const char *getProgressInfo() {
    // 读一个文件
    char *path = (char *) calloc(1, PATH_MAX);
    char *line = (char *) calloc(1, THREAD_NAME_LENGTH);
    snprintf(path, PATH_MAX, "/proc/%d/stat", getpid());
    FILE *commFile = NULL;
    if (commFile = fopen(path, "r")) {
        fgets(line, THREAD_NAME_LENGTH, commFile);
        fclose(commFile);
    }
    if (line) {
        int length = strlen(line);
        if (line[length - 1] == '\n') {
            line[length - 1] = '\0';
        }
    }
    LOGE("progress info ->%s", line);
    free(path);
    return line;
}
// 线程 stat 信息
extern const char *getThreadInfo() {
    // 读一个文件
    char *path = (char *) calloc(1, PATH_MAX);
    char *line = (char *) calloc(1, THREAD_NAME_LENGTH);
    snprintf(path, PATH_MAX, "/proc/%d/task/%d/stat", getpid(), gettid());
    FILE *commFile = NULL;
    if (commFile = fopen(path, "r")) {
        fgets(line, THREAD_NAME_LENGTH, commFile);
        fclose(commFile);
    }
    if (line) {
        int length = strlen(line);
        if (line[length - 1] == '\n') {
            line[length - 1] = '\0';
        }
    }
    LOGE("thread info ->%s", line);
    free(path);
    return line;
}

写在最后:

效能优化这东西其实可做可不做,不像需求能快速的看到收益和效果,所以这也是很多同学比较缺失的一个部分。为什么我们要看重这点,因为今天市场上比较成功的公司基本都做到了"一拖三" 。首先,是团队很强 - 创始人和团队很强,在一个比较强的团队带领下,需要做到另外三点,要么是把用户体验提升了、要么能降低成本、要么能提升效率,有的时候我们的成本也没下降,效率也没提升,但是如果能把用户体验做得极致,也可以。总之,在一个优秀的、成功的团队基础之上,我们只要能够把用户体验、能够把成本或者效率这三者至少做到一点,同时另外两点又没有减损的话,基本上就可以成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容