《java并发编程实战笔记》
第十二章 并发程序的测试
测试并发程序而言,所面临的主要挑战在于:潜在的错误发生具有不确定性,需要比普通的串行程序测试更广的范围并且执行更长的时间。
并发测试大致分为两类:安全性测试和活跃性测试。
安全测试 ----- 通常采用测试不变性条件的形式,即判断某个类的行为是否与其他规范保持一致。
活跃性测试 ----- 包括进展测试和无进展测试两个方面(很难量化)。
性能测试----- 性能测试与活跃性测试相关,主要通过:吞吐量、响应性、可伸缩性衡量。
正确性测试
测试并发类设计单元测试时,首先要执行与测试串行类时相同的分析----找出需要检查的不变性条件与后验条件。(不变性条件:判断状态是有效还是无效,后验条件:判断状态改变后是否有效)。接下来讲通过构建一个基于Semaphore来实现的缓存的有界缓存,测试缓存的正确性。
基本单元测试(基于信号量有界缓存BoundedBuffer例子)
知识铺垫(Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”)
BoundedBuffer 用一个泛型数组、Semaphore 实现了一个固定长度的、可以缓存队列可删除可插入个数的队列。availableItems表示可以从缓存中删除的元素个数。availableSpaces表示可以插入到缓存的元素个数,初始值等于缓存的大小。分析:
availableItems 设置为0,要求任何线程在accquire之前要release保证了队列必须插入有值才能take
availableSpaces 初始值为大小为capacity,表明队列最大值为capacity,同时也表明一开始最多有capacity个线程可以同时put队列,当然随着队列插入可同时插入的线程变少。
在最关键的插入、取出队列的操作中,采用synchronized 包装两个方法,保证了同步性。
说明:在实际使用中,肯定不可能自己编写一个有界缓存,但是此例实现的思路值得学习。如果实际需要使用有界缓存,应该直接使用ArrayBlockingQueue或者LinkedBlockingQueue。
public class BoundedBuffer<E> {
//可用信号量、空间信号量
private final Semaphore availableItems, availableSpaces;
private final E[] items;//缓存
private int putPosition = 0, takePosition = 0;//放、取索引位置
public BoundedBuffer(int capacity) {
availableItems = new Semaphore(0);//初始时没有可用的元素
availableSpaces = new Semaphore(capacity);//初始时空间信号量为最大容量
items = (E[]) new Object[capacity];
}
public boolean isEmpty() {
//如果可用信号量为0,则表示缓存为空
return availableItems.availablePermits() == 0;
}
public boolean isFull() {
//如果空间信号量为0,表示缓存已满
return availableSpaces.availablePermits() == 0;
}
public void put(E x) throws InterruptedException {
availableSpaces.acquire();//阻塞获取空间信号量
doInsert(x);
availableItems.release();//可用信号量加1
}
public E take() throws InterruptedException {
availableItems.acquire();
E item = doExtract();
availableSpaces.release();
return item;
}
private synchronized void doInsert(E x) {
int i = putPosition;
items[i] = x;
putPosition = (++i == items.length) ? 0 : i;
}
private synchronized E doExtract() {
int i = takePosition;
E x = items[i];
items[i] = null;//加快垃圾回收
takePosition = (++i == items.length) ? 0 : i;
return x;
}
}
先进行基本的单元测试:该基本的单元测试相当于串行上下文中执行的测试,测试了BoundedBuffer的所有方法,间接验证其后验条件和不变性条件。
public class BoundedBufferTest extends TestCase {
//刚构造好的缓存是否为空测试
public void testIsEmptyWhenConstructed() {
BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10);
assertTrue(bb.isEmpty());
assertFalse(bb.isFull());
}
//测试是否满
public void testIsFullAfterPuts() throws InterruptedException {
BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10);
for (int i = 0; i < 10; i++)
bb.put(i);
assertTrue(bb.isFull());
assertFalse(bb.isEmpty());
}
对阻塞行为与对中断响应的测试
在测试并发的基本属性时,需要引入多个线程。大多数测试框架并不能很好的支持并发性测试,测试阻塞行为,当然线程被阻塞不再执行时,阻塞才是成功的,为了让阻塞行为效果更明显,可以在阻塞方法中抛出异常。
当阻塞发生后,要使方法解除阻塞最简单的方式是采用中断,可以在阻塞方法发生后,线程阻塞后再中断它,当然这要求阻塞方法提取返回或者抛出InterrupedException来响应中断。
例如BoundedBuffer的阻塞行为以及对中断的响应性测试,如果从空缓存中获取一个元素,如果take方法成功,表明测试失败。在等待“获取”一段时间后,再中断该线程,如果线程在调用Object类的wait()、 join() 或者sleep()方法被强行中断,那么将会抛出InterruptedException。
public class TestBoundedBufferBlock extends TestCase {
public void testTakeBlocksWhenEmpty() {
final BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>