深入了解Java并发——《Java Concurrency in Practice》12.并发程序的测试

Posted 在咖啡里溺水的鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入了解Java并发——《Java Concurrency in Practice》12.并发程序的测试相关的知识,希望对你有一定的参考价值。

本章节的内容介绍了如何对并发程序进行测试,与普通程序进行测试的区别,测试过程中应当注意的问题,可以使用的技巧和方法等

12.1 正确性测试

在为并发类设计单元测试时,首先需要执行与测试串行类时相同的分析——找出需要检查的不变形条件和后验条件。

12.1.1 基本的单元测试

进行并发程序测试时,包含一组串行测试通常是有帮助的,它们有助于在开始分析数据竞争之前就找出与并发性无关的问题。

12.1.2 对阻塞操作的测试

在测试方法的阻塞行为时,将引入额外的复杂性:当方法被成功的阻塞后,还必须使方法解除阻塞。实现这个功能的一种简单的方式就是使用中断——在一个单独的线程中启动一个阻塞的操作,然后等到线程阻塞后再中断它,然后宣告阻塞操作成功。这要求阻塞方法通过提前返回或抛出InterruptedException来相应中断。

“等待并指导线程阻塞后”,必须估计执行这些指令可能需要多长的事件,并且等待的事件会更长。

使用Thread.getState方法来验证线程能否再一个条件等待上阻塞是不可靠的,因为被阻塞线程并不需要进入WAINTING或TIMED_WAITING等状态,JVM可以选择通过自旋等待来实现阻塞。Object.wait、Condition.await等方法上因为存在伪唤醒,所以即使一个线程等待的条件尚未成真,也可能从WAITING或TIMED_WAITING等状态临时性的转换到RUNNABLE状态。Thread.getState的返回结果不能用于并发控制,它将限制测试的有效性——其主要作用还是作为调试信息的来源。

12.1.3 安全性测试

在构建对并发类的安全性测试中,需要解决的关键问题在于,要找出那些容易检查的属性,这些属性在发生错误的情况下极有可能失败,同时又不会使得错误检查代码认为的限制并发性。理想情况是,在测试属性中不需要任何同步机制。

由于大多数随机数生成器 RNG Random Number Generator 类都是线程安全的,并且会带来额外的同步开销,因此在随机数生成的过程中,可能会与被测试类在执行时序之间产生耦合关系。因此使用一个通用的RNG,不如使用一些简单的伪随机函数。因为我们并不需要某种高质量的随机性,二值需要确保在不同的测试运行中都有不同的数字,用来欺骗编译器不去进行程序优化。

**适合在测试中使用的随机数生成器

static int xorShift(int y) 
    y ^= (y << 6);
    y ^= (y >>> 21);
    y ^= (y << 7);
    return y;

要最大程度的检测出一些对执行时序敏感的数据竞争,那么测试中的线程数量应该多余CPU数量,这样在任意时刻都会有一些线程在运行,而另一些被交换出去,从而可以检测线程间交替行为的可预测性。

12.1.4 资源管理的测试

通过一些测量应用程序中内存使用情况的堆检查工具,可以很容易的测试出对内存的不合理占用,许多商业和开源的堆分析工具中都支持这种功能。

12.1.5 使用回调

在构造测试案例时,对客户提供的代码进行回调是非常有帮助的。回调函数的执行通常是对在对象声明周期的一些已知位置上,并且在这些位置上非常适合判断不变形条件是否被破坏。

通过使用自定义的线程工厂,可以对线程的创建过程进行控制。

12.1.6 产生更多的交替操作

并发代码中的大多数错误都是一些低概率事件,因此测试并发错误时需要反复的执行多次。有些方法可以提高发现这些错误的概率。

有一种有用的方法可以提高交替操作的数量,以便能更有效的搜索程序的状态空间:在访问共享状态的操作中,使用Thread.yield将产生更多的上下文切换。这项技术的有效性与具体的平台相关。这种方法需要在测试中添加一些调用并且在正式产品中删除这些调用,通过AOP工具,可以降低这种不便性。

12.2 性能测试

性能测试通常是功能测试的延伸。在性能测试中应该包含一些基本的功能测试,从而确保不会对错误的代码进行功能测试。

性能测试与功能测试的不同在于,性能测试将衡量典型测试用例中的端到端性能。

性能测试的第二个目标是根据经验值来调整各种不同的限值。这些限值可能依赖于具体平台的特性,因此需要动态的进行配置,我们需要合理的选择这些值,从而使程序能够在更多的系统上良好的运行。

12.2.1 计时功能

可以借助于栅栏动作来测量启动和结束时间。

12.2.2 多种算法比较

java.util.concurrent中的算法已经通过类似的测试进行了调优,其性能也已经达到我们已知的最佳状态。

由于内存分配操作通常是线程本地的,因此如果算法能通过多执行一些内存分配操作来降低竞争程度,那么这种算法通常具有更高的可伸缩性。

12.2.3 响应性衡量

有时候我们需要知道某个动作经过多长时间才能完成,这就要测量服务时间的变化情况。可预测性同样是一个非常有价值的性能特征。

通过表示任务完成时间的直方图,最能看出服务时间的变动。

非公平的信号量:隐蔽栅栏 shaded bars
公平的信号量:开放栅栏 open bars

除非线程由于密集的同步需求而被持续的阻塞,否则非公平的信号量通常能实现更好的吞吐量,而公平的信号量则实现更低的变动性。因为这些结果之间的差异非常大,所以Semaphore要求客户选择针对哪一个特性进行优化。

12.3 避免性能测试的陷阱

12.3.1 垃圾回收

垃圾回收的执行时序是无法预测的。在执行测试时,垃圾回收期可能在任何时刻运行。

垃圾回收操作会对性能测试的结果带来很大的影响。有两种策略可以防止垃圾回收操作对测试结果产生偏差。
- 第一种策略是,确保垃圾回收操作在测试运行的整个期间都不会执行,在调用JVM时指定 -verbose:gc 来判断是否执行了垃圾回收操作
- 第二种策略是,确保垃圾回收操作在测试期间执行多次,这样测试程序就能充分反映出运行期间的内存分配与垃圾回收等开销。

通常第二种策略更好,它要求更长的测试时间,并且更有可能反应实际环境下的性能。

12.3.2 动态编译

在HotSopt JVM及其他现代JVM中将字节码的解释与动态编译结合起来使用。当某个类第一次被加载时,JVM会通过解译字节码的方式来执行它。在某个时刻,如果一个方法运行的次数足够多,那么动态编译器会将它编译为机器代码,当编译完成后,代码的执行方式将从解释执行变成直接执行。

这种编译的执行时机是无法预测的。只有在所有代码都编译完成以后,才应该统计测试的运行事件。基于各种原因,代码还还有可能被反编译以及重新编译。

解决方式:
- 使程序运行足够长的时间,这样编译过程以及解释执行都只是总运行时间的很小一部分
- 使代码先运行一段时间并且不测试这段时间内的代码性能

在HotSpot中,可以在执行时添加 -xx:+PrintCompilation ,使动态编译运行时输出一条信息,可以通过这些消息来验证动态编译的执行时机。

通过在同一个JVM中将相同的测试运行多次,可以验证测试方法的有效性。第一组结果应该作为“预先执行”的结果而丢弃,如果在剩下的结果中仍然存在不一致的地方,那么就需要进一步对测试进行分析,从而找出结果不可重复的原因。

12.3.3 对代码路径的不真实采样

运行时编译器根据收集到的信息对已编译的代码进行优化。在某些情况下,JVM可能会基于一些知识临时有效的假设进行优化,并在这些假设失效时抛弃已编译的代码。

因此,测试程序需要尽量覆盖在该应用程序中将执行的代码路径集合。否则,动态编译器可能会针对一个单线程测试程序进行一些专门的优化,但在真实的并发环境中,这些优化都会不复存在。

因此,即便只是想测试单线程的性能,也应该将单线程的性能测试与多线程的性能测试结合在一起。

12.3.4 不真实的竞争程度

并发应用程序可以交替执行两种不同类型的工作:访问共享数据和执行线程的本地计算。根据两种不同类型工作的相关程度,在应用程序中将出现不同程度的竞争,并表现出不同的性能与可伸缩性。

要活的有实际意义的结果,在并发性能测试中应该尽量模拟典型应用程序中的线程本地计算量以及并发协调开销。

12.3.5 无用代码消除

编译器会自动优化掉死代码

要编写有效的性能测试程序,就需要告诉优化器不要将基准测试当做无用代码而优化掉。这就要求在程序中对每个计算结果都要通过某种方式来使用,这种方式不需要同步或者大量的计算。

一个简单的技巧是计算某个派生对象中域的散列值,并将它与一个任意值进行比较,比如System.nanoTime。

if (foo.x.hashCode() == System.nanoTime()) 
    System.out.print(" ");

不仅每个计算结果都应该被使用,而且还应该是不可预测的。

12.4 其他的测试方法

12.4.1 代码审查

代码审查不仅能发现错误,还能提高描述实现细节的注释的质量,因此将降低后期维护的成本和风险。

12.4.2 静态分析工具

Findbugs

12.4.3 面向切面的测试技术

AOP可以用来确保不变性条件不被破坏,或者与同步策略的某些方面保持一致。

12.4.4 分析与监测工具

内置的JMX提供了一些有限的功能来监测线程的行为。

以上是关于深入了解Java并发——《Java Concurrency in Practice》12.并发程序的测试的主要内容,如果未能解决你的问题,请参考以下文章

深入了解Java并发——《Java Concurrency in Practice》14.构建自定义的同步工具

基于JVM原理JMM模型和CPU缓存模型深入理解Java并发编程

深入了解Java并发——《Java Concurrency in Practice》10.避免活跃性危险

深入了解Java并发——《Java Concurrency in Practice》11.性能与可伸缩性

深入了解Java并发——《Java Concurrency in Practice》8.线程池的使用

一文让你深入了解 Java-Netty高性能高并发