腾讯bugly干货分享经典随机crash之一:线程安全

腾讯Bugly 腾讯Bugly     2022-08-27     427

关键词:

本文作者:鲁可——腾讯SNG专项测试组 测试工程师

背景

Android QQ 在2016下半年连着好几个版本二灰 Crash 率都很高,如果说有新需求,一灰的 Crash 率高,还能找点理由,可是开发童鞋解过一灰的 Crash 单后,为啥二灰还有这么高的 Crash 率,我们还有覆盖全 SNG、不少外 BG 明星产品的终端稳定性测试工具 NewMonkey 随身版(NewMonkey系腾讯内部研发的测试工具,外部app有兴趣请点击这里填问卷调查申请使用)每天都在跑,更何况大多 Top Crash 都发生在用户使用很普通、很频繁的场景,实在令人匪夷所思,那段时间抄送各老板的运营邮件 Crash 率数据天天标红,项目组人心惶惶,发个版本感觉要烧高香,当时作为 Android NewMonkey 核心成员的我更是压力山大,在这样的背景下,我临危受命,负责研究外网 Top Crash,尽可能找到一些共性问题,在研究过程中,得到开发的大多反馈是:

  1. 问开发:这个 Top Crash 能找到复现场景吗?答:场景就在这里,但就是复现不了

  2. 这里有个线程安全问题,那我加个同步;这里有个空指针,那我就加个判空

一时间我也陷入深深的困扰:

  1. 代码是开发写的,开发都复现不了,我更复现不了啊

  2. 会 Crash 的代码在那,开发就改了,完全是头痛医头脚痛医脚的做法,作为一个测试,我还能做啥呢?

当时的心情真的是如图所示:

然而作为一名专项测试,如果只是看到这些表象,是远远不够的,也感谢老大一直对我的激励:“一切你复现不了的 Crash,那都是你没有找到问题的根源。”我当时给自己的目标是“一定要复现,有条件要上,没有条件创造条件也要上。”

线程安全问题的现状

《Bugly2016移动应用质量大数据报告》提到:

空指针异常在Java代码中最为常见,不出所料,NullPointerException依然是最常见的Java异常,该异常影响面广但容易修复,开发者想快速降低崩溃率可以优先解决此类异常。相较于2015年,IllegalStateException从5%提升至10%,OutOfMemoryError从3%提升至6%。

——数据源自腾讯Bugly

IllegalStateException主要是由线程引起的,本篇就线程安全类问题与您一探究竟,我将向您展示研究过程中的乐趣以及最终取得的效果,另外解密我申请的两个线程领域的专利。

我们先来看一种具有代表性的Crash,这里以一次灰度的Top 1 Crash为例子,至于这个Crash的引入原因,开发童鞋为了修改性能bug,将方法放到了线程中执行,我省去中间几百行代码,抽取出代码梗概。

类中声明了一个成员变量mTask

getDrawable会被多次调用,ThreadManager是Android QQ线程管理组件,用ThreadManager提交了一个Runnable任务,run()里调用decodeBigImage做解码,new一个AsyncTask对象,然后execute

首先说明同一个AsyncTask实例不能execute多次,否则就会报:

java.lang.IllegalStateException: Cannot execute task: the task is already running

Top Crash中正是在decodeBigImage方法中mTask.execute那一行报的这个错,开发童鞋的解法,那就很自然了,虽然不知道怎么Crash的,先将decodeBigImage加了同步,反正不会Crash了,况且当时紧急情况下,也容不得多想。

请您静思几秒,想想上面的代码不加同步可能会有什么问题,这个Top Crash开发、测试同学一度觉得十分诡异,实在想不出哪里会有问题,mTask怎么会执行多次呢?代码里每次都有new对象啊,然后用新建出来的对象execute,怎么会有问题呢?

问题的剖析

问题的分析,分析的一些方法无非就是从日志、代码逻辑、原理上着手了。

如果您当初像我一样,没啥思路,不妨先做一道笔试题吧:

i=0,两个线程分别执行i++,可能的结果有1、2

解释:i++不是原子操作,每次要先把i从内存读取到寄存器,然后++,然后再把寄存器中的值写回到内存中,这需要至少3步。

可能出现的情况:

Case1:

thread1 读到0,寄存器加1,写回内存1

thread2 读到0,寄存器加1,写回内存1

结果:1

Case2:

thread1 读到0,寄存器加1,写回内存1

thread2 读到1,寄存器加1,写回内存2

结果:2

到这里,您或许有点思路了,因为我们潜意识把decodeBigImage()看成了原子操作,然而真实情况并非如此。

如果是两个线程同时并发,一共有4种情况,我用图给您展示两种:

两个线程在并发的情况下,用排列组合的知识,很容易算出发生Crash的概率是50%,那这个概率还是蛮高的,如果更多数量线程并发,Crash概率更高,那也就不难理解这个Crash是Top 1 Crash了。

问:那为啥我们复现不了?

答:因为我省掉的几百行代码中,随时有if else分支有可能return掉,并且cpu瞬息万变,我们手工很难构造出线程并发的条件。

如果到这里,对临界资源访问的方法加了同步,这个Crash就算解决了,那下次碰到这类问题,都要等出了问题后,再加同步吗?那这个代价有点太高了,况且Crash 我还没复现出来呢。

问题可能的解决方案

1.监控临界资源的变更记录

既然问题发生在一个类成员变量有多处对它修改,出现了覆盖写的情况,那监控变量值变更记录,似乎是个有效的监控手段,但请教了专业做静态代码扫描codedog的同学,行业内貌似没有成熟的解决方案,动态执行时做这个变量值监控似乎难度不小。

那么多变量哪些该监控?怎么判断出值变更有问题的?怎么避免误报?这些都有不小的难度。

2. 在执行语句上暂停

既然是给mTask赋值时出现的问题,一个线程执行后,那我们在这条语句上暂停,像调试一样,等其他线程来覆盖第一个线程的赋值结果,那这个Crash就能完整重现了,可这个方案依旧有不小的难度,那么多赋值语句,哪些需要暂停?怎么动态在语句执行时暂停?怎么释放?要解决好这些问题,难度依旧不小。

3. 模拟线程并发

既然这类线程安全的问题是在多线程并发时出现问题的概率大,避免发生Crash就加同步,避免线程并发访问临界资源,如果要在事发前发现这类问题,那我们就应该反其道而行之,增大线程并发的概率。由于有hook技术,对方法执行前后能做手脚,似乎有切入点。

考虑到方案3已是我们能想到最容易实现的一个方案了,最终我们采用了方案3,但依然有不少问题要解决。

一般线程执行情况是这样的:

3.1、哪些线程需要并发?

因为有些线程八竿子打不着,没有竞争关系,根本就没必要让它们并发。

我们这里先把范围局限在同一个方法启动的多个线程对同一个资源有竞争关系,归为同一类线程;如果是在不同方法里开启多个线程对同一个资源并发访问,这种情况更加复杂,静态分析做检查,都有很高的误报率,动态分析更加难做,暂时不在我们考虑范围内。

3.2、怎么区分不同类别的线程?

应用程序中启动线程的地方不相同,则认为是不同类型的线程,我们用调用堆栈区分不同类型的线程。

3.3、假设同时想让n个线程并发,怎么让它们在执行前都停住,然后让它们同时执行?

找到线程真正执行的地方,在执行前加一种计数器锁,如果计数值达到n后,再释放锁,加计数器锁后效果:

3.4、如果线程请求数达不到n,又如何让已加锁的线程同时执行?

加一个倒计时锁,如果等待超过设定时间,则自行释放锁,计数器、倒计时锁同时作用的效果如图所示:

这个方案可能带来的影响:

  1. 性能上,毫无疑问,由于我们暂停了线程的执行,肯定是有影响的

  2. 兼容性上,由于采用的都是通用的hook技术,并发SDK已集成到NewMonkey随身版中,已稳定运行了好几个月,稳定性得到了保证。

线程的并发方案

Java里新建线程主要有两种方式:通过实现 Runnable 接口;通过继承 Thread 类本身;实现 Runnable 接口也要被Thread封装了然后再去执行,总之两种方式,启动最终都是靠Thread.start(),执行都是靠Thread.run(),这就好办了,线程的并发方案分两步,如下图所示:

Hook start获取调用堆栈,将同一调用堆栈的tid聚在一类。

通过上面处理,我们能对拥有同一key值、不同tid的线程加同一个锁

到此第一个专利水到渠成:一种模拟线程并发的方法

中间踩过一些坑,分享给大家:

问题1、为啥不hook Runnable?

答:因为Runnable是接口,只能hook类

问题2、为啥不hook start来获取调用堆栈时就模拟并发?

答:1、线程真正执行时是在run里 2、start是个同步方法,在这里加锁也没法模拟并发

问题3、为啥不hook run来获取调用堆栈、并且模拟并发?

答:因为被开发者调用的是start(),能拿到app的调用堆栈,以此区分不同类型的thread,hook run获取到的都是系统堆栈,无法做线程特征区分。

线程池的并发方案

自己写了个Thread的demo,发现并发凑效了,本以为到此就大功告成了,可以模拟出Top Crash了,结果发现并非如此,像手Q这么大的项目是不太允许随便通过new Thread方式新建线程的,Runnable任务大多通过线程池调度来执行。

由于线程池的原理比线程复杂,我觉得线程池核心思想是最大程度复用了存活的线程,限于篇幅,这里我对线程池不再多做赘述,给大家推荐几篇不错的文章:

聊聊并发(三)——JAVA线程池的分析和使用
http://oa5504rxk.bkt.clouddn.com/utest_week33/www.infoq.com/cn/articles/java-threadPool

Java线程池架构原理和源码解析(ThreadPoolExecutor)
http://oa5504rxk.bkt.clouddn.com/utest_week33/blog.csdn.net/xieyuooo/article/details/8718741

我画了一个线程池流程图,以帮助理解下面的hook并发方案

总之,结论就是:

  1. 开发者通过ThreadPoolExecutor.execute(runnable) 提交Runnable任务

  2. runnable任务执行前会执行ThreadPoolExecutor.beforeExecute(Runnable)

在此不得不感叹老外设计接口时的缜密和深思熟虑。

我们的线程池模拟并发方案仍然分两步:

Hook execute获取调用堆栈,将同一调用堆栈的runnable的hashcode聚在一类。

通过上面处理,我们能对拥有同一key值、不同hash的Runnable加同一个锁

您可能一咋眼看上去跟上面那个方案好像很接近,其实有着本质上的区别,正好也可以回答为啥上面hook Thread start run不能解决线程池并发的问题

答:原因有两点:

  1. 我通过hook Thread拿不到线程池启动线程的调用堆栈,因为线程池至始至终就没有把Thread暴露给开发者

  2. 线程池里的Worker(Worker是对Thread的再封装)与Runnable不再具有一一绑定的关系,Worker以领任务的方式去执行Runnable,同一堆栈特征的Runnable究竟由哪个Worker执行的,跟设定线程数量、采用何种缓冲队列、每个Runnable执行耗时、这些Worker的状态都有关系,只能通过Runnable自身加调用堆栈去区分。

第二个专利因此也水到渠成:一种模拟通过线程池调度的线程并发方案

那这个方案能否替换线程并发那个方案呢?不能,由于Thread和Runnable一一绑定,可以将线程并发方案中的线程tid换成Runnable实例的hashcode,但是hook Thread还是必须要做的。

手Q的线程池基于线程池进一步做了封装,做了很多非常深入、实用的改造,更加强大。

效果

最终,我们将IllegalStateException Crash的占比由Android QQ 6.5.0的8%下降至6.6.0的1%

我诚惶诚恐,冠上“经典”二字,是为了博人眼球,文章若有纰漏,欢迎大家指教,两个专利的适用范围我想了下,也不仅仅适用于Android终端,前端、客户端、后台,所有平台应该都适用,大家可以按照自己平台去实现。

道高一尺魔高一丈,在降Crash率上,依旧任重而道远。

NewMonkey系腾讯内部研发的测试工具,外部app有兴趣请点击这里申请使用


更多精彩内容欢迎关注腾讯 Bugly的微信公众账号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

腾讯bugly干货分享深入源码探索reactnative通信机制

...y技术干货系列内容主要涉及移动开发方向,是由Bugly邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处。本文从源码角度剖析RNA中Java<>Js的通信机制(基于最新的RNARele... 查看详情

腾讯bugly干货分享安卓单元测试:what,whyandhow

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57d28349101cd07a5404c415DevClub是一个交流移动开发技术,结交朋友,扩展人脉的社群,成员都是经过审核的移动开发工程师。每周都会举行嘉宾... 查看详情

腾讯bugly干货分享你为什么需要kotlin

本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/xAFKGarHhfQ3nKUwPDlWwQ一、往事曾经你有段时间研究Intellij的插件开发,企图编译IntellijIdeaCommunityEdition(ICE)的源码,结果发现有个... 查看详情

腾讯bugly干货分享从0到1打造直播app

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/5811d42e7fd6ec467453bf58作者:李智文概要分享内容:互联网内容载体变迁历程,文字——图片/声音——视频——VR/AR——…….。从直播1.0秀场... 查看详情

腾讯bugly干货分享微信android热补丁实践演进之路

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1264&extra=page%3D1继插件化后,热补丁技术在2015年开始爆发,目前已经是非常热门的Android开发技术。其中比较著... 查看详情

腾讯bugly干货分享美团大众点评hybrid化建设

本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/rNGD6SotKoO8frmxIU8-xw本期T沙龙探讨了移动端热更新相关的话题。由于沙龙时间的限制,本期我们选取了美团的Hybrid化建设、去哪儿... 查看详情

腾讯bugly干货分享html5视频直播一站式扫盲

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1277视频直播这么火,再不学就out了。为了紧跟潮流,本文将向大家介绍一下视频直播中的基本流程和主要的技术... 查看详情

腾讯bugly干货分享精神哥手把手教你怎样智斗anr

上帝说要有ANR,于是Bugly就有了ANR上报。那么ANR究竟是什么?近期非常多童鞋问起精神哥ANR的问题,那么这次就来聊一下,鸡爪怎么泡才好吃。噢不,是怎样高速定位ANR。ANR是什么简单说,通常就是App执行的时候,duang~卡住了。... 查看详情

腾讯bugly干货分享彻底弄懂http缓存机制-基于缓存策略三要素分解法

本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/qOMO0LIdA47j3RjhbCWUEQ作者:李志刚导语Http缓存机制作为web性能优化的重要手段,对从事Web开发的小伙伴们来说是必须要掌握的知识... 查看详情

腾讯bugly干货分享webvr如此近-three.js的webvr示例解析

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57c7ff1689a6c9121b1adb16作者:苏晏烨关于WebVR最近VR的发展十分吸引人们的眼球,很多同学应该也心痒痒的想体验VR设备,然而现在的专业硬件... 查看详情

腾讯bugly干货分享webvr如此近-three.js的webvr示例解析

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57c7ff1689a6c9121b1adb16作者:苏晏烨关于WebVR最近VR的发展十分吸引人们的眼球,很多同学应该也心痒痒的想体验VR设备,然而现在的专业硬件... 查看详情

腾讯bugly干货分享基于webpack&vue&vue-router的spa初体验

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57d13a57132ff21c38110186导语最近这几年的前端圈子,由于戏台一般精彩纷呈,从MVC到MVVM,你刚唱罢我登场。backbone,angularjs已成昨日黄花,reac... 查看详情

腾讯bugly干货分享基于webpack&vue&vue-router的spa初体验

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57d13a57132ff21c38110186导语最近这几年的前端圈子,由于戏台一般精彩纷呈,从MVC到MVVM,你刚唱罢我登场。backbone,angularjs已成昨日黄花,reac... 查看详情

「干货分享」经典设计模式之单例模式

设计模式千千万,总是单例最常见。单例模式的定义保证一个类仅有一个实例,并提供一个访问它的全局访问点。六种单例的创建方式1.饿汉式publicclassSingleton{privatestaticSingletoninstance=newSingleton();privateSingleton(){}publicstaticSingletongetI... 查看详情

集成腾讯bugly日志收集接入详细步骤和错误解决方案--android

Bugly是腾讯公司为移动开发者开放的服务之一,这里主要指Crash监控、崩溃分析等质量跟踪服务。一、登录BUGLY官网1、登录BUGLY官网以后,选择新建产品,选择IOS或ADNROID平台,如图: 完事以后点击保存,点击当... 查看详情

stm32经典概述(干货)

STM32经典概述(干货)首先,在学习Cortex-M3时,我们必须要知道必要的缩略语。  在网上看的,觉得挺好的,分享过来了整理如下: AMBA:先进单片机总线架构  ADK:AMBA设计套件 ,AHB:先进高性能总线  ... 查看详情

干货分享:学习c++编程,那些经典书籍是你一定会用上的呢?

很多同学在学习C/C++的时候,知道自己要书籍结合视频对照学习,网上搜索了也没找到,但却不知道自己应该看那几本书籍,本文特意针对C++的几个方面,全面的例举了学习时候对你有帮助的书籍&#... 查看详情

多线程十大经典案例之一双线程读写队列数据

本文配套程序下载地址为:http://download.csdn.net/detail/morewindows/5136035转载请标明出处,原文地址:http://blog.csdn.net/morewindows/article/details/8646902欢迎关注微博:http://weibo.com/MoreWindows 在《秒杀多线程系列》的前十五篇中介绍多线程... 查看详情