多线程bug学习记录
Posted 360技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程bug学习记录相关的知识,希望对你有一定的参考价值。
奇技指南
作为测试人员在面临并发逻辑的需求时常常难以切入,时常见到程序引入新的bug,如数据竞争和死锁,非常难以发现或复现。今天送大家一份多线程bug学习记录,希望对大家有所帮助。
本文首发于Qtest之道,已授权转载。
作为测试人员在面临并发逻辑的需求时常常难以切入,时常见到程序引入新的bug,如数据竞争和死锁,非常难以发现或复现。许多程序员并不知道特定多线程编程方法的微妙细节,而这可能会导致代码错误。我们的目标是发现程序潜在的问题,学习并发bug就得提上日程了。
并发bug的分类与定义
数据竞争
当两个线程尝试访问相同的共享内存,且其中至少有一个访存操作是写操作,且这两个访存操作的执行顺序没有被同步语句强制保证。
常见的数据竞争导致的crash
在该代码片段中,数据竞争的存在可能会导致程序发生读取空指针的错误。
Tread 1 本来是需要读取对象 oneMan 中的 name 属性,即字符串 Jame。但是Tread 2 中将对象 oneMan 释放并置为 NULL 了,这就使得Tread 1 在读取对象oneMan 的 name 属性时有可能会发生空指针异常。
数据竞争是最常见的并行bug。
原子性失效
原子性指的是某一个线程中对相同共享内存的多个访存操作,其并行执行效果和顺序执行的效果是一样的。 当有其他线程访问相同的共
享内存破坏这种原子性的时候,原子性失效发生了。
Java的数据竞争示例
这个例子中线程T1和T2在调度器的影响下会同时对共享变量shared[0]做操作。
正常情况下线程T1读取shared[0]的值执行L1,然后T2读取shared[0]的值,执行L2,shared[0]会按照预期结果执行两次自增操作+2;
但是如果T1和T2先读取了shared[0]的值,然后分别执行了L1和L2,shared[0]的值会错误的自增1,L1或L2的自增操作丢失。
这个例子的bug同时符合原子性失效和数据竞争的特征。
有些语言的操作我们会误认为是原子操作而忽略了在多线程下的隐患,就如上面例子中的代码是等效于我们最常用的对i的操作,例如
等,还有其他(read-modify-write)操作,如下图的一些原子性失效的线程交织。
顺序性失效
开发可能会假设两个线程中的两个访存操作存在着一种特定顺序,当这种顺序由于线程调度顺序的不确定性而被破坏的时候顺序性失效就发生了。
这是一个完成写入操作的案例,预期的顺序是线程1初始化标志位io_pending,在循环中执行一些操作后,再由线程2将io_pending置为false。但是有可能线程2过早执行导致循环一次都没有被执行,导致逻辑错误。
死锁
开发通常会使用锁来保护对共享变量的访问。
当多个线程由于相互之间需要获取对方的资源而又获取不到的时候,这些线程就会被一直阻塞,这时候死锁发生了。
下面的代码就会产生一个死锁:
classTest有两个实例objA和objB,线程T1调用objA.addAll(objB),线程T2调用objB.addAll(objA)。假设T1程获得了 objB 的锁,但是它开始执行之前,第T2就开始执行方法,同时获取了 objA的 锁。结果,Bom!!!!!每一个线程都会等待另一个线程所保持的锁。这个案例被称作对称死锁,Java 1.4 版本上,其中Collections.synchronized 方法返回的一些同步容器会发生死锁。
以上是常见的四大类多线程并行bug,随着编码技术的发展,在一些研究性论文中还提到了一些非常见bug:饥饿、活锁、锁护送、优先级倒置等(有兴趣研究的同学可以查看末尾附上的链接)。
并发bug的规避手段
四大类并发bug类型是有重叠的,我们可以聚焦在一些公共原因的地方去规避这些问题。
谨慎的使用全局标志位
从上面的几个代码案例中可以感觉到,公共变量是重灾区,是三大bug类型的重叠区域。
开发在写代码时遇到需要保存程序状态的时候,经常会出现使用一个全局的静态标志flag来表示。
在多线程环境下这无疑是给自己埋雷,可读性极差的if-else、多线程下混乱难以复现的bug种子是对程序未来业务增长的极大隐患。
在此强烈推荐一个编程模式来替代标志位:状态机!!!
android系统在原生应用中有大量应用,甚至包装了个工具类供大家使用(com.android.internal.util.StateMachine 基于Android 8.0源码包名)。
谷歌的使用案例:(Android/packages/services/Telecomm/src/com/android/server/telecom/ CallAudioModeStateMachine.java)。
保证内存可见性
例如java的volatile关键字,来每次修改变量后,立即将变量写回主内存,每次使用变量时,必须从主内存中同步变量的值。
保证加锁顺序
在一个逻辑中对于需要对多个对象加锁的场景,需要每个线程都使用一致的加锁顺序。如线程一需要对A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,破坏死锁的产生条件。
良好的try-catch逻辑
许多开发遇到难以复现或者log过少的bug经常加个try-catch(Exception)了事。
这样的代码无疑在增加测试暴露并发问题的难度,也很难减少bug发生时对业务逻辑的冲击。
针对多线程并发bug,很重要的一点就是在内测环境提前暴露。
开发应该根据代码优先捕获指定类型的异常,然后增加处理对应异常的现场逻辑。
下面的代码就是一个比较好的实践
对创建的线程命名
这一点已经不能算是规避手段了,但是出了bug好定位啊。
因为最近在业务测试上遇到一些困难,尤其前端这块儿对各种异步请求的场景,测试得我是醉生梦死!~!~
决定在业余研究学习下,上面就是自己对并发bug学习的记录和总结,很浅显。
未来自己可能会多多学习并发bug的检测手段和技术,业务测试上的难点就是我们不断学习前进的动力。也欢迎大家多留言交流~
捏哈哈!!!
——作者
非常见并行bug的资料
锁护送:
https://blog.csdn.net/michael_r_chang/article/details/30717763
锁饥饿、活动锁:
https://blog.csdn.net/moudaen/article/details/14487809;https://stackoverflow.com/questions/6155951/whats-the-difference-between-deadlock-and-livelock
优先级倒置:
https://blog.csdn.net/maimang1001/article/details/7343045
参考文献:
《并发缺陷的检测与规避研究》
《并行bug监测系统研究》
《阿里巴巴java开发手册》
界世的你当不
只作你的肩膀
无
技术干货|一手资讯|精彩活动
空·
以上是关于多线程bug学习记录的主要内容,如果未能解决你的问题,请参考以下文章