第十二章:并发程序的测试——Java并发编程实战
并发程序中潜在错误的发生并不具有确定性,而是随机的。
安全性测试:通常会采用测试不变性条件的形式,即判断某个类的行为是否与其规范保持一致
活跃性测试:进展测试和无进展测试两方面,这些都是很难量化的(性能:即吞吐量,响应性,可伸缩性测试)
一、正确性测试
重点:找出需要检查的不变性条件和后验条件
1、对基本单元的测试——串行的执行
1 public class BoundedBufferTests { 2 3 @Test 4 public void testIsEmptyWhenConstructed(){ 5 BoundedBuffer<String> bf = new BoundedBuffer<String>(10); 6 assertTrue(bf.isEmpty()); 7 } 8 9 @Test 10 public void testIsFullAfterPuts() throws InterruptedException{ 11 BoundedBuffer<String> bf = new BoundedBuffer<String>(10); 12 for (int i=0; i<10; i++){ 13 bf.put("" + i); 14 } 15 assertTrue(bf.isFull()); 16 assertTrue(bf.isEmpty()); 17 } 18 }
2、对阻塞操作的测试
每个测试必须等他创建的所有线程结束后才可以结束(join)
要测试一个方法的阻塞行为,类似于测试一个抛出异常的方法:如果这个方法可以正常返回,那么就意味着测试失败。
在测试方法的阻塞行为时,将引入额外的复杂性:当方法被成功地阻塞后,还必须使方法解除阻塞。(中断)
1 public void testTaskBlocksWhenEmpty(){ 2 final BoundedBuffer<Integer> bb = new BoundedBuffer<>(10); 3 Thread taker = new Thread(){ 4 @Override 5 public void run() { 6 try { 7 int unused = bb.take(); 8 fail(); //不应执行到这里 9 } catch (InterruptedException e) { 10 } 11 } 12 }; 13 try { 14 taker.start(); 15 Thread.sleep(1000); 16 taker.interrupt(); 17 taker.join(2000); //保证即使taker永久阻塞也能返回 18 assertFalse(taker.isAlive()); 19 } catch (InterruptedException e) { 20 fail(); 21 } 22 }
3、安全性测试
构建对并发类的安全性测试中,需要解决的关键问题在于,要找出那些容易检查的属性,这些属性在发生错误的情况下极有可能失败,同时又不会使得错误检查代码人为地限制并发性。理想的情况是,在测试属性中不需要任何同步机制
例:通过计算入列和出列的校验和进行检验(使用栅栏保证线程均运行到可检验处再检验)
4、资源管理测试
对于任何持有或管理其他对象的对象,都应该在不需要这些对象时销毁对它们的引用
例:使用堆检验工具对内存资源使用进行检验
5、使用回调
可以通过自定义扩展类来进行相关测试
1 public class TestingThreadFactory implements ThreadFactory { 2 public final AtomicInteger numCreated = 3 new AtomicInteger(); //记录创建的线程数 4 private final ThreadFactory factory = 5 Executors.defaultThreadFactory(); 6 7 @Override 8 public Thread newThread(Runnable r) { 9 numCreated.incrementAndGet(); 10 return factory.newThread(r); 11 } 12 }
6、产生更多的交替操作
使用yield、sleep命令更容易使错误出现
二、性能测试
性能测试的目标:
- 衡量典型测试用例中的端到端性能,获得合理的使用场景
- 根据经验值来调整各种不同的限值,如线程数量,缓存容量等
1、计时器
通过增加计时器,并改变各个参数、线程池大小、缓存大小,计算出运行时间
例:
2、多种算法的比较
使用不同的内部实现算法,找出具有更高的可伸缩性的算法
例:
3、响应性衡量
某个动作经过多长时间才能执行完成,这时就要测量服务时间的变化情况
除非线程由于密集的同步需求而被持续的阻塞,否则非公平的信号量通常能实现更好的吞吐量,而公平的信号量则实现更低的变动性(公平性开销主要由于线程阻塞所引起)
三、避免性能测试的陷阱
1、垃圾回收
- 保证垃圾回收在执行测试程序期间不被执行,可通过-verbose:gc查看垃圾回收信息。
- 保证垃圾回收在执行测试程序期间执行多次,可以充分反映出运行期间的内存分配和垃圾回收等开销。
2、动态编译
- 可以让测试程序运行足够长时间,防止动态编译对测试结果产生的偏差。
- 在HotSpot中设置-xx:+PrintCompilation,在动态编译时输出一条信息
3、对代码路径不真实采样
- 动态编译可能会让不同地方调用的同一方法生成的代码不同
- 测试程序不仅要大致判断某个典型应用程序的使用模式,还要尽量覆盖在该应用程序中将执行的代码路径集合
4、不真实的竞争程度
- 不同的共享数据和执行本地计算的比例,将表现出不同的竞争程度,也就有不同的性能和可伸缩性
5、无用代码的消除
- 编译器可能会删除那些没有意义或不会产生结果或可预测结果的代码
- 使结果尽量是不可预测的
四、其他测试方法
代码审查(人工检查代码),竞态分析工具(FindBugs,CheckStyle),面向方面的测试技术,分析与检测工具(jvisualvm)