一、问题现象
1、用户直观看到的现象是System先ANR。
2、ANR之后系统重启。
测试方法:
在录音的界面不停的滑动音量进度条,同时座机给测试机打电话,电话没有接通,只见界面冻结,弹出ANR,接着系统重启。
Platform:MT6732
Android版本:4.4.4KK
BuildType:user
系统软件版本:SWA3A+UMA0
系统RAM:1GB
问题概率:≈2%
参考机行为:
1、低概率问题,暂无参考机行为。
二、解决方案
通过初步分析、深入分析(具体分析过程、关键代码和log在下面会附上)我们清楚的知道了问题发生的原因:
1、SystemServer进程中的AudioService相关的setindex和getindex等方法会在多个Thread中进行,而这些方法都是以synchronized关键字声明的,即这些方法会以类的每个实例中的lock进行多线程同步。
2、这些synchronized的方法中存在穿插调用其他同类型实例的synchronized方法的行为。
3、完全将方法用synchronized关键字声明是一个比较懒的做法,这样会导致同步锁的粒度太大,没有细化临界区,在多线程高并发的状态下会降低代码流执行的吞吐量,而且会加大在穿插调用过程中彼此依赖发生死锁碰撞的可能。
在当前代码的执行状态下就有一定概率(很小,依赖于具体的进程调度时机)出现因为调度原因而先调度到了Thread1并执行了对象A的同步方法,然后调度到了Thread2并执行对象B的同步方法,在对象B的同步方法中想要去调用对象A的同步方法,此时发生阻塞,接着又调度到了Thread1,继续执行对象A的同步方法中的代码,然后又去调用对象B的同步方法,因为在Thread2中已经持有了对象B的lock,所以此时Thread1也发生了阻塞,当前的状态就是Thread1和Thread2相互等待对方释放锁而无限期等待,各种的代码流得不到执行而死锁。
考虑AudioService中复杂的逻辑,所以要以最小风险的改动来修复这个问题,因此这里给出的方案没有进行太大的改动,而且比较显而易见的是全部加synchronized关键字的这些出问题的代码,AOSP还有一定的优化空间。
最终针对以上问题的根本原因,我们给出以下解决方案:
1、更换同步锁的类型
在需要同步的临界代码区使用类的全局锁来代替每个实例自己的锁,从而保证多个Thread在相互穿插调用时不会发生死锁。
2、方案相关的具体代码和backtrace
以上是发生死锁时锁对应的backtrace调用栈以及相应的代码,通过红线圈住部分我们可以看到发生问题时的关键调用关系和状态。
3、最终方案的代码修改
三、问题初步分析
以ALTO4.5TMO出问题时候的一份典型backtrace和log为例,发现出问题时SystemServer的主线程block在了一个AudioService内部的一个函数上,从而引发ANR和SWT重启,具体backtrace如下:
为什么会block?通过查看如上对应代码,发现这个方法是个synchronized的,而且方法中在满足条件时还会遍历并调用同类型但是不同实例对象的synchronized方法,因此这里被block就需要满足一个条件:调用的同类型不同实例对象的synchronized方法无法进入,即在其他thread中已经进入了这个synchronized方法。
根据这个线索继续查看SystemServer中与AudioService相关的thread的调用栈,找到Binder_2这个thread,具体的backtrace如下:
通过backtrace和对应代码我们发现Binder_2这个thread也block在了一个AudioService内部的synchronized函数中,同样的这个函数中在满足一定的条件时也会调用同类型不同实例的synchronized方法。
四、深入分析问题
经过初步分析我们定位到了第一个问题点,即两个不同的Thread都block在了同一个类型的synchronized方法上,同时也产生了1个问题,接下来我们继续深入分析以期能到找到答案和问题的根本原因。
1、两个Thread为什么会同时block?
通过进一步分析和查看代码发现,由于两个Thread所执行的都是synchronized方法,如果它们由于调度和执行原因而产生了相互依赖的关系,那么就会发生同时block的现象而死锁,由于backtrace只能看到调用关系,不能知道运行时各个对象实例的状态,所以我们根据backtrace模拟systemserver中当前这两个thread的问题状态,结果完全匹配当前的问题现象,具体的模拟代码如下:
先自定义一个Thread类,接收两个TestSync类的实例并在run里面调用实例1的同步方法,同时将实例2传递过去。
接着定义一个TestSync类,并定义两个synchronized的成员函数,然后在每个函数开始的地方都先sleep 10ms,以满足进程调度切换的状态。
最后在activity的onResume方法中进行测试,结果测试的activity就会ANR,为什么会ANR?
原理和上面systemServer ANR并SWT重启一样,这里activity的UI主线程和新建的ct1线程发生了死锁。
以上代码的执行流程大致如下:
1、新建t1,t2两个TestSync类的实例以及CThread类的实例ct1并将t1和t2传递过去
2、启动ct1这个thread
3、无论是ct1的代码流先被调度到执行还是UI主线程继续执行都会进入t1或者t2的synchronized方法。
4、这里假设ct1在start之后立马被调度到并执行了t1的synchronized方法,然后sleep 10ms,此时再次发生调度。
5、UI主线程被再次调度到,然后执行t2的synchronized方法,sleep 10ms,再次调度到其它thread。
6、等到ct1的10ms sleep先结束之后再次调度到ct1,然后执行t2的synchronized方法,这里会发生阻塞,因为在UI主线程中已经进入到了t2的synchronized方法,即t2实例自己的lock已经处于锁定状态,然后调度到其他thread。
7、等到UI主线程的10ms sleep结束之后再次调度到UI主线程,然后执行t1的synchronized的方法,这里同样会发生阻塞,因为在ct1中已经进入t1的synchronized方法,t1实例自己的lock已经处于锁定状态,然后调度到其他thread。
8、此时ct1和UI主线程已经产生相互依赖而死锁。
将上面的代码中使用的synchronized关键字更改为同步类的全局锁,问题解决,activity不会再发生ANR,具体更改如下:
五、解决方案潜在的影响
由于使用类的全局锁,而且没有细分临界区,所以在高并发的情况下可能会略微降低代码执行流的吞吐量,但是这个影响对SystemServer中AudioService的setindex和getindex等方法可以忽略,因为这几个方法都非常轻量级并且并发量不会达到太高的量级。
Analyzed by vincent.song from SWD2 Framework team.
[email protected]
201506241646
版权声明:本文为博主原创文章,未经博主允许不得转载。