第12章 并发程序的测试
吞吐量:指一组并发任务中已完成任务所栈的比例。
响应性:指请求从发出到完成之间的时间(也成为延迟)。
12.1 正确性测试
在为某个并发类设计单元测试时,首先需要执行与测试串行类时相同的分析---找出需要检查的不变性条件和后验条件。
12.1.1 基本的单元测试
在测试集中包含一组串行测试通常是有帮助的,因为它们有助于在开始分析数据竞争之前找出与并发无关性的问题。
12.1.2 对阻塞操作的测试
在测试并发的基本属性时,需要引入多个线程。
Thread.getState的返回结果不能用于并发控制,它能限制测试的有效性---其主要作用还是作为调试信息的来源。
12.1.3 安全性测试
在构建对并发类的安全性测试中,需要解决的关键问题在于,要找出那些容易检查的属性,这些属性在发生错误的情况下极有可能失败,同时又不会使得错误检查代码认为地限制并发性。理性情况是,在测试属性中不需要任何同步机制。
12.1.4 资源管理的测试
12.1.5 使用回调
回调函数的执行通常是在对象生命周期的一些已知位置上,并且在这些位置上非常适合判断不变性是否被破坏。
12.1.6 产生跟多的交替操作
在访问共享状态的操作中,使用Thread.yield将产生更多的上下文切换(这项技术是平台相关的)。
12.2 性能测试
12.2.1 在PutTakeTest中增加计时功能
12.2.2 多种算法的比较
响应性衡量
如果能获取更小的服务时间变动性,那么更长的平均时间是有意义的,“可预测需性”同样是一个非常有价值的性能特征。
除非线程由于密集的同步需求而被持续地阻塞,否则非公平的信号量通常能实现更好的吞吐量。
12.3 避免性能测试的陷阱
12.3.1 垃圾回收
垃圾回收的执行时序是无法预测的,因此在执行测试时,垃圾回收器可能在任何时刻运行。
12.3.2 动态编译
当某个类第一次被加载时,JVM会通过解释字节码的方式执行它。在某个时刻,如果一个方法运行的次数足够多,那么动态编译器会将它编译为机器代码,当编译完成后,代码的执行方式将从解释执行变成直接执行。
12.3.3 对代码路径的不真实采集
运行时编译器根据收集到的信息对已编译的代码进行优化。JVM可以与执行过程特定的信息来生成更优的代码,这个意味着在编译某个程序的方法M时生成的代码,将可能与编译另一个不同程序中的方法M时生成的代码不同。
12.3.4 不真实的竞争程度
并发的应用程序可以交替执行两种不同的类型的工作:访问共享数据(例如从共享工作队列中取出下一个任务)以及执行线程本地的计算(例如,执行任务,并假设任务本身不会访问共享数据)。
12.3.5 无用代码的消除
要编写有效的性能测试程序,那就需要告诉优化器不要将基准测试当作无用代码而优化掉。这就要求在程序中对每个计算结果都要通过某种方式来使用,这种方式不需要同步或者大量的计算。
12.4 其他的测试方法
测试的目标不是更多地发现错误,而是提高代码按照预期方式工作的可信度。
12.4.1 代码审查
多人参与的代码审查通常是不可替代的(另一方面,代码审查也不能取代测试)。
12.4.2 静态分析工具
静态分析工具能生成一个警告列表,其中包含的警告信息必须通过手工方式进行检查,从而确定这些警告是否表示真正的错误。
FindBUgs包含的检测器:
- 调用Thread.run()
- 未被释放的锁
标准的做法是在一个人finally块中释放显式锁,否则,当发生Exception事件时,锁仍然处于未被释放的状态。 - 空的同步块
- 双重检查加锁
双重检查加锁时一种错误的习惯用法,其初衷是为了降低延迟初始化过程中的同步开销,该用法在读取一个共享的可变域时缺少正确的同步。 - 在构造函数中启动一个线程。
如果在构造函数中启动一个线程,那么将可能带来子类化问题,同时还会导致this引用从构造函数中逸出。 - 通知错误
如果一个同步块中调用了notify或notifyAll,但没有修改任何状态,那么就可能出错。 - 条件等待中的错误
如果在调用Object.wait和Condition.await方法时没有持有锁,或者不在某个循环中,或者没有检查某些状态谓词,那么通常是一个错误。 - 对Lock和Condition的误用
- 在休眠或者等待的同时持有一个锁。
- 自旋循环。
当等待某个状态转换发生时,闭锁或条件等待通常比在代码中自旋是一种更好的技术。
12.4.3 面向方面的测试技术
AOP可以用来确保不变性条件不被破坏,或者与同步策略的某些方面保持一直。
12.4.4 分析与监测工具
分析工具通常为能给出程序内部的详细信息,为每个线程提供一个事件线显示,并且用颜色区分不同的线程状态(可运行,由于等待某个锁为阻塞,由于等待I/O操作而阻塞等等)。
小结
要测试并发程序的正确性可能非常困难,因为并发程序的许多故障模式哦都是一些低概率事件,它们对于执行时序、负载情况以及其他难以出现的条件都非常敏感。