12_并发程序测试

并发程序测试

一、并发程序测试

1.1 并发测试的挑战

  • 潜在错误的发生具有不确定性,是随机的

1.2 并发测试的类型

1.2.1 安全性测试

  • 安全性:不发生任何错误的行为
  • 安全性测试:通常采用测试不变性条件和后验条件的形式,即判断某个类的行为在并发测试下,其状态是否与其规范一致

1.2.2 活跃性测试

  • 活跃性:某个“良好”的行为“终究”会发生
  • 活跃性测试与性能测试息息相关,活跃性测试比较难量化,所以常用性能测试替代
  • 性能测试可通过多个方面来衡量:
    • 吞吐量:一组并发任务在已完成任务中所占的比例
    • 响应性(延迟):请求从发出到完成所用的时间
    • 可伸缩性:在增加更多资源的情况下,吞吐量的提升情况

二、正确性测试

2.1 正确性测试的基本思路

  • 找出需要检查的不变性条件和后验条件
  • 为不变性条件和后验条件编写测试用例
  • 在编写测试用例时,不断探索发现新的测试情况
  • 为新的情况编写测试用例

2.2 常用的测试方法

2.2.1 直接验证测试

  • 直接调用对象的对外接口方法,验证它的不变性条件和后验条件
1
2
3
4
void testIsEmpty() {
// ...
assertTrue(list.isEmpty());
}

2.2.2 回调钩子测试

  • 有些对象拥有自己的一些生命周期,不同时期的状态有固定的规范
  • 比如线程的生命周期可以是:创建、启动、阻塞、结束
  • 可以在对象生命周期中添加一些钩子函数,在每个阶段调用测试方法,验证当时的状态是否正确

2.2.3 随机安全测试

  • 想要测试在不可预测的并发访问情况下,执行结果是否一直是安全正确的
  • 需要多次验证,并且每次验证应该是随机的,但是结果应该总是安全正确的
  • 随机性结果的验证,可以通过校验和计算函数来实现,即对入参和返回进行校验比较
  • 大多数随机生成器类都是线程安全的,使用它们会带来额外的同步开销,使用一些简单的随机生成器更好

简单的随机数生成函数(基于 hashCode + nanoTime + xorShift):

1
2
3
4
5
6
7
8
9
int randomInt(Object object) {
return xorShift(object.hashCode() * System.nanoTime());
}
int xorShift(int y) {
y ^= (y << 6);
y ^= (y >>> 21);
y ^= (y << 7);
return y;
}

2.2.4 阻塞中断测试

  • 如果某个方法在特殊条件下进入了阻塞,那么只有当线程不再执行时,测试才是成功的
  • 测试阻塞方法的方式:
    • 在一个单独线程中启动阻塞方法
    • 等待线程进入阻塞状态
    • 在另一个线程中调用中断,将阻塞线程中断唤醒
  • 阻塞方法要求能够响应中断并返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void testBlock() {
// ...
Thread t = new Thread(() -> {
try {
// 做某些阻塞操作...
fail();
} catch (InterruptedException e) {
}
});
try {
t.start();
// 等待线程进入阻塞状态
Thread.sleep(LOCKUP_DETECT_TIMEOUT);
// 中断阻塞线程
t.interrupt();
// 等待线程结束
t.join(LOCKUP_DETECT_TIMEOUT);
// 验证线程是否已经结束
assertTrue(t.isAlive());
} catch (InterruptedException e) {
fail();
}
}

2.2.5 资源管理测试

  • 对于任何持有或管理其他对象的对象,应该在不需要的时候释放和销毁它们的引用
  • 并发线程很多时,将会非常消耗资源,最终可能导致资源耗尽和应用程序故障失败
  • 为了防止资源耗尽,需要测试线程拥有的资源是否已经正确释放
  • 测试资源的占用和释放,可以采用一些分析工具,分析内存中的对象信息,比如堆中对象的垃圾回收情况

2.2.6 上下文切换测试

  • 并发问题的来源,就是因为线程之间的资源竞争
  • 为了提高并发错误发生的可能性,可以提高线程之间的上下文切换频率
  • 可以通过使用 Thread.yield() 方法来产生更多的交替操作

三、性能测试

3.1 性能测试目的

  • 通过衡量测试用例的性能,反映出对象在应用程序中的实际用途,比如并发容器适用于高并发的情况
  • 根据测试结果,调整各种不同的限制参数,比如线程数量、缓存容量等

3.2 常用测试方法

3.2.1 计时功能测试

  • 性能最直接的方式,就是计算并发任务的完成时间
  • 所有并发任务总的完成时间、各个任务的完成时间、操作平均完成时间
  • 计时测试,可能需要用到闭锁(latch)、栅栏(Barrier)等方式来同时为各个任务计时

3.2.2 算法对比测试

  • 通过对比其他的算法实现,来衡量当前算法的性能
  • 比如对比同步容器和并发容器的性能,就能明显看出各自的性能差异

3.2.3 稳定性测试

  • 除了任务完成的速度,还应该测试任务在各种情况下的稳定性
  • 比如,算法 A 的完成时间在 10ms ~ 200ms 之间,算法 B 的完成时间在 70ms ~ 100ms 之间,就说明算法 B 更具稳定性
  • 有时候,稳定性比速度更具有价值,所以应视情况而定

四、并发测试的陷阱

4.1 垃圾回收

  • 垃圾回收的执行无法预测,可以在任何时刻运行,测试不应该考虑垃圾回收的触发时机
  • 防止垃圾回收造成测试偏差:
    • 无垃圾回收:确保垃圾回收不会执行(通过打印gc日志查看)
    • 平均垃圾回收:确保垃圾回收多次执行,基本平均到每次测试中,减少误差
  • 平均垃圾回收策略,在实际环境中的表现会更好

4.2 动态编译

  • 通常情况,JVM 都是通过解释字节码的方式来运行程序
  • 但是如果某个方法频繁调用,JVM 的动态编译器会直接将方法编译为机器代码,后面直接就运行机器代码,不再解释字节码运行了
  • 在某些情况下,动态编译执行还可能回退回解释执行
  • 动态编译具有更好的性能,但是这会对性能测试造成一定的影响
  • 避免动态编译影响的方式:
    • 避免动态编译:测试应具有更普遍的随机性,避免动态编译的触发
    • 错开动态编辑:等到动态编译之后,再开始测试性能
  • 如果动态编译不可避免,那么性能测试就应该延迟到等动态编译完成后再开始

4.3 针对性优化

  • JVM 会根据实际的执行过程,来生成更优的代码
  • 也就是说,同一套代码,在不同环境下执行,被 JVM 优化后的代码可能会有不同
  • 测试应覆盖执行代码的所有分支情况,避免 JVM 针对某种情况进行针对性优化
  • 比如,如果执行某个方法时,一直都是只执行某条代码分支,多次执行以后,JVM 有可能会针对它进行代码优化

4.4 无用代码的消除

  • 优化编译器,可能会消除对最终结果没有任何影响的无用代码
  • 测试代码中,有些代码虽然不影响最终的结果,但也是必要的,它不应该被优化器给优化掉
  • 可以通过查看机器代码,来验证测试代码是否被优化了
  • 避免无用代码被优化掉的小技巧:使用对象的 hashCode 与任意值(如 nanoTime)比较
1
2
3
4
if (object.hashCode() == System.nanoTime()) {
// 输出一个无用的消息
System.out.println(" ");
}

4.5 不真实竞争

  • 测试程序,应该与实际使用相适应,否则测试结果就会显得不真实,毫无意义
  • 如果实际使用中,任务都是用于计算密集型的,那么测试程序应该是计算密集型的

五、其他测试方法

5.1 代码审查

  • 并发专家能够比大多数测试程序更高效地发现一些竞争问题(毕竟测试代码也是人写的)
  • 通过找出常见的设计性问题,还可以提高代码的质量
  • 可以提前发现问题,降低后期维护的成本和风险

5.2 静态代码分析

  • 一些代码扫描工具(如 findBug),可以自动发现一些常见的代码错误
  • 不一致的同步:加锁对象不一致,导致加锁无用
  • 未释放的锁:锁释放没有放 finally 块中,异常时可能导致锁永远无法释放
  • 调用 Thread.run():不要调用 Thread 的 run() 方法,而是调用 start() 方法
  • 双重校验锁:错误的习惯用法,写不好就会出现一堆问题
  • Lock 的误用:Lock 并不是作为同步块来使用的
  • 自旋循环:自旋等待不加 volatile 可能会导致 CPU 无限高负载,闭锁和条件等待通常是一种更好的方式

5.3 面向方面技术

  • 通过切面编程,去验证在各个阶段的状态和线程
  • 并发领域的切面技术还不够成熟,作用有限

5.4 分析与监测工具

  • 存在一些分析工具,可以给出程序内部的详细执行信息
  • 比如线程状态、线程堆栈、线程锁对象、线程等待、死锁分析等
  • 分析工具通常采用侵入式实现,有可能会对程序的执行时序和行为产生较大的影响
作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议