抖音优化Java多线程线上锁监控方案的应用范围特点与使用方法抖音雪碧同学的抖音号

背景

Java多线程开发中为了保证数据的一致性,引入了同步锁()。 但是过度使用锁可能会导致卡住的问题甚至ANR:

本文将重点介绍在线锁监控方案的原理和使用方法,以及我们在抖音上发现的锁的经典案例和优化实践。

监控程序

获取运行时锁信息的方式有以下几种:

计划

应用范围

特征

离线

可以发现锁导致的耗时没有调用栈

自定义ROM

离线

支持调用栈修改ROM​​。 门槛高,只支持特定机型。

离线

只支持 + 设备不支持数据包,性能开销大

考虑到很多锁问题需要暴露一定规模的在线用户,没有调用栈,很难从根本上定位和解决在线用户的锁问题。 最后,我们开发了一套在线锁监控系统,需要满足以下需求:

这样的锁监控系统可以帮助我们高效定位和解决线上问题,实现防变质。

锁监控原理

先说一类常见的耗时,其实就是ART虚拟机输出的锁信息。

简单介绍一下里面的信息

monitor contention with owner work_thread (27176) at android.content.res.Resources android.app.ResourcesManager.getOrCreateResources(android.os.IBinder, android.content.res.ResourcesKey, java.lang.ClassLoader)(ResourcesManager.java:901) waiters=1 blocking from java.util.ArrayList android.app.ActivityThread.collectComponentCallbacks(boolean, android.content.res.Configuration)(ActivityThread.java:5836)

Java锁,无论是方法还是块,虚拟机终究会到来。 我们重点介绍6,分别在锁的开头和结尾调用(...)和()

在线课程

默认关闭,开关在()。 我们设置为可以启动当前进程的ART虚拟机。

再看(...)和()的实现,其实就是用来把字符串写成一个特殊的(/sys////)。

所以通过hook.so的方法,压,实现了(...)和()的拦截。 with and END可以计算出阻塞时长,解析with ... log可以得到我们关心的Java锁信息。

获取堆栈

至此,我们已经可以监控到在线用户的锁问题。 但这还不够。 为了优化锁的性能,我们需要知道等待锁的具体原因,也就是Java调用栈。

要获取 Java 调用堆栈,可以使用 .() 方法。 因为我们hook了虚拟机的等待锁线程,此时线程处于一种特殊状态快速排名,不能通过JNI直接调用Java方法,否则会造成线上问题。

解决办法是异步获取栈,5ms后通知子线程去抢栈,计算阻塞时间,和栈数据一起放入队列,等待上报。时间不满足5ms,取消堆栈捕获和报告

数据平台

由于方案本身有一定的性能开销,我们在灰度测试中只对部分用户开启锁监控。 配置在线采样后,命中用户自动开启锁监控,数据上报平台后即可消费数据。

具体情况可以查看设备信息、阻塞时长、调用栈

根据调用栈找到源码,可以定位到哪个锁抖音优化,说明上报的数据是准确的。

稳定性方面,10万灰度用户开启锁监控后,没有出现新的稳定性问题。

优化实践

经过多轮锁的收集和管理,我们取得了一些不错的收益。 下面介绍几个锁管理的典型案例。

典型

锁:

我们先分析一下什么是:解析xml生成View树的过程称为进程。 这是一个耗时的过程。 常规的方法是通过异步的方式来减少它在主线程中的耗时,大大减少卡顿、页面打开和启动的时间; 但是这种方法也会带来新的问题,比如方法中有代码块被锁保护。 并行构建会造成锁等待,可能会增加主线程的耗时。 这个问题有以下三种解决方案:

代码构建代替xml构建定制

文件目录锁:

获取目录(,,DB, and)的实现有两个关键耗时点: 1.IPC(.)和文件是否存在; 2.锁定“”保护; 因此,ipc的长度和并发存在可能会导致App卡顿,如图Anr数据:

相关的常用API包括 , , 等。考虑到系统的一些目录一般是不会变化的,我们可以对一些不会变化的目录进行处理,减少带锁方法块的执行,有效绕过锁等待。

:

子线程与主线程通信的常用方式是在主线程中插入一个task(),等待这个task()被主线程调度执行; 因此,它会锁定并保护对消息列表的修改,主要实现在和next方法中。

利用在线锁信息收集,基于这些信息,我们可以很方便地追踪到持有锁的线程,最后根据情况将()移动到子线程,这样可以大大降低主线程的压力和被锁的可能性等待锁。 这个问题的修改方法并不复杂,关键是如何监控这些加锁线程。

序列化和反序列化:

抖音中一些常用的数据对象以Json格式存储。 为了保证这些数据的完整性,在读取和存储的时候加了锁保护,导致锁等待比较常见,这种情况在启动场景尤为明显; 因此,为了减少锁等待,需要加快序列化和反向序列化,我们针对这个问题做了三个优化方案:

锁:

获取、大小、或者xml等资源的最终实现基本都封装在. 为了保证数据的正确性,加入了锁(对象)保护。 一般调用关系如图:

常用的调用点有:

随着xml异步的增加,这些方法的并发调用也随之增加,导致主线程的锁等待越来越突出,最终导致卡顿。 针对这个问题,我们目前的优化方案主要有:

所以加载锁优化:

提供给load so的接口实现都封装在里面,比如常用and,如图1和图2,这个方法是加锁的,如果并发加载so会造成锁等待。 通过监控数据,我们验证了这个问题,也有一些意想不到的收获。 例如抖音优化,平台可能有自己的,因此需要添加:

根据so的不同情况,我们主要有以下优化思路:

:

在收集到的数据中,我们还发现了一些系统层面的框架锁,如下图所示:

这个问题主要集中在启动阶段。 ams会向中间发送一个trim通知,收到通知后,会在列表中添加一个trim任务(这个任务列表不会展开)网站优化,即下一个到来时执行;

trim过程主要包括收集、、、发送trim消息给他们,同时也是系统提供业务清理自身内存的机会; 采集过程通过()进行保护,如图:

考虑到启动阶段不太关心内存的释放,可以在启动阶段尽量不要执行trim操作,比如40秒以内; 具体实现如下,先替换可以接管的操作,在任务列表中每次激活或删除trim操作的40秒内启动。

收入

在抖音,除了对上面列出的典型锁进行了优化,我们还对业务本身的一些锁进行了优化,有的已经通过线上实验验证了收益,有的还在试用阶段; 分析也证实了锁优化可以带来启动、流畅等技术收益,间接带来良好的商业收益,这也加强了我们在这个方向上的持续探索和深化。

概括

上面列举的只是一些有代表性的通用Java锁,在实际开发中遇到的要多得多,但不管是哪一种锁,根据进程和代码归属,可以分为以下四类:业务锁、依赖锁图书馆锁、框架锁和系统锁;

不同类型的锁优化思路会有所不同。 有些解决方案可以重复使用,有些只能逐案解决。 具体优化方案包括:减少调用、绕过调用、使用读写锁、不加锁。

分类

描述

过程

代码

优化

业务锁

源码可见,可直接修改; 比如之前的序列化优化。

应用进程

包括

直接优化; 静态aop

依赖库锁

包含编译好的产品,可以修改

应用进程

包括

直接优化; 静态aop

框架锁

运行时加载,同时存在兼容性; 比如前面提到的锁、锁、锁

应用进程

不含

减少通话; 动态aop

系统锁

系统为App提供的服务和资源,App之间存在竞争,所以服务层需要加锁保护,如IPC、文件系统、数据库等。

服务流程

不含

减少通话

总结

经过半年的摸索和优化,该方案已经上线,作为我们日常防劣化和主动优化的输入工具。 我们的评价点主要包括以下四点:

虽然这个方案只能监控锁,比如CAS、锁、wait是不能监控的,但是锁在我们日常开发中占了非常大的比重,所以基本满足了我们的大部分需求。 当然,我们也在继续探索其他Lock的监控和验证其价值。

免责声明:本站所有文章和图片均来自用户分享和网络收集,文章和图片版权归原作者及原出处所有,仅供学习与参考,请勿用于商业用途,如果损害了您的权利,请联系网站客服处理。