您的位置:首页 > 编程语言 > Java开发

《Java并发编程实战》第十二章 并发程序的测试 读书笔记

2014-06-02 21:03 363 查看
并发测试大致分为两类:安全性测试(不发生任何错误的行为)和活跃性测试(某个良好的行为终究会发生)。
安全测试 - 通常采用测试不变性条件的形式,即判断某个类的行为是否与其他规范保持一致。
活跃性测试 - 包括进展测试和无进展测试两个方面。
性能测试与活跃性测试相关,主要包括:吞吐量、响应性、可伸缩性。

一、正确性测试

找出需要检查的不变条件和后延条件。

import java.util.concurrent.Semaphore;

public class BoundedBuffer<E> {

private final Semaphore availableItems, availableSpaces;
private final E[] items;
private int putPosition = 0;
private int takePosition = 0;

@SuppressWarnings("unchecked")
public BoundedBuffer(int capacity) {
availableItems = new Semaphore(0);
availableSpaces = new Semaphore(capacity);
items = (E[]) new Object[capacity];
}

public boolean isEmpty() {
return availableItems.availablePermits() == 0;
}

public boolean isFull() {
return availableSpaces.availablePermits() == 0;
}

public void put(E x) throws InterruptedException {
availableSpaces.acquire();
doInsert(x);
availableItems.release();
}

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;
}

}


1 基本的单元测试

import static org.junit.Assert.*;
import org.junit.Test;

public class BoundedBufferTests {

@Test
public void testIsEmptyWhenConstructed() {
BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10);
assertTrue(bb.isEmpty());
assertFalse(bb.isFull());
}

@Test
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());
assertTrue(bb.isEmpty());
}
}


2 对阻塞操作的测试
take方法是否阻塞、中断处理。从空缓存中获取一个元素。
@Test
public void testTakeBlocksWhenEmpty(){
final BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10);
Thread taker = new Thread(){
@Override
public void run() {
try {
int unused =  bb.take();
fail(); //如果执行到这里,那么表示出现了一个错误
} catch (InterruptedException e) { }
}
};
try {
taker.start();
Thread.sleep(LOCKUP_DETECT_TIMEOUT);
taker.interrupt();
taker.join(LOCKUP_DETECT_TIMEOUT);
assertFalse(taker.isAlive());
} catch (InterruptedException e) {
fail();
}
}


创建一个“获取”线程,该线程将尝试从空缓存中获取一个元素。
如果take方法成功,那么表示测试失败。
执行测试的线程启动“获取”线程,等待一段时间,然后中断该线程。
如果“获取”线程正确地在take方法中阻塞,那么将抛出InterruptedException,而捕获到这个异常的catch块将把这个异常视为测试成功,并让线程退出。
然后,主测试线程会尝试与“获取”线程合并,通过调用Thread.isAlive来验证join方法是否成功返回,如果“获取”线程可以响应中断,那么join能很快地完成。

使用Thread.getState来验证线程能否在一个条件等待上阻塞,但这种方法并不可靠。被阻塞线程并不需要进入WAITING或者TIMED_WAITING等状态,因此JVM可以选择通过自旋等待来实现阻塞。

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

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import junit.framework.TestCase;

public class PutTakeTest extends TestCase {
private static final ExecutorService pool = Executors.newCachedThreadPool();
private final AtomicInteger putSum = new AtomicInteger(0);
private final AtomicInteger takeSum = new AtomicInteger(0);
private final CyclicBarrier barrier;
private final BoundedBuffer<Integer> bb;
private final int nTrials, nPairs;

public static void main(String[] args) {
new PutTakeTest(10, 10, 100000).test(); // 示例参数
pool.shutdown();
}

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

public PutTakeTest(int capacity, int nPairs, int nTrials) {
this.bb = new BoundedBuffer<Integer>(capacity);
this.nTrials = nTrials;
this.nPairs = nPairs;
this.barrier = new CyclicBarrier(nPairs * 2 + 1);
}

void test() {
try {
for (int i = 0; i < nPairs; i++) {
pool.execute(new Producer());
pool.execute(new Consumer());
}
barrier.await(); // 等待所有的线程就绪
barrier.await(); // 等待所有的线程执行完成
assertEquals(putSum.get(), takeSum.get());
} catch (Exception e) {
throw new RuntimeException(e);
}
}

class Producer implements Runnable {
@Override
public void run() {
try {
int seed = (this.hashCode() ^ (int) System.nanoTime());
int sum = 0;
barrier.await();
for (int i = nTrials; i > 0; --i) {
bb.put(seed);
sum += seed;
seed = xorShift(seed);
}
putSum.getAndAdd(sum);
barrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

class Consumer implements Runnable {
@Override
public void run() {
try {
barrier.await();
int sum = 0;
for (int i = nTrials; i > 0; --i) {
sum += bb.take();
}
takeSum.getAndAdd(sum);
barrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}


4 资源管理的测试
对于任何持有或管理其他对象的对象,都应该在不需要这些对象时销毁对他们的引用。测试资源泄露的例子:
class Big {
double[] data = new double[100000];
};

void testLeak() throws InterruptedException{
BoundedBuffer<Big> bb = new BoundedBuffer<Big>(CAPACITY);
int heapSize1 = /* 生成堆的快照 */;
for (int i = 0; i < CAPACITY; i++){
bb.put(new Big());
}
for (int i = 0; i < CAPACITY; i++){
bb.take();
}
int heapSize2 = /* 生成堆的快照 */;
assertTrue(Math.abs(heapSize1 - heapSize2) < THRESHOLD);
}


5 使用回调

6 产生更多的交替操作

二、性能测试

性能测试的目标 - 根据经验值来调整各种不同的限值。例如:线程数量、缓存容量等。

1 在PutTakeTest中增加计时功能
基于栅栏的定时器
this .timer = new BarrierTimer();
this .barrier = new CyclicBarrier(nPairs * 2 + 1, timer);

public class BarrierTimer implements Runnable{
private boolean started ;
private long startTime ;
private long endTime ;

@Override
public synchronized void run() {
long t = System.nanoTime();
if (!started ){
started = true ;
startTime = t;
} else {
endTime = t;
}
}

public synchronized void clear(){
started = false ;
}

public synchronized long getTime(){
return endTime - startTime;
}
}


修改后的test方法中使用了基于栅栏的计时器
void test(){
try {
timer.clear();
for (int i = 0; i < nPairs; i++){
pool .execute( new Producer());
pool .execute( new Consumer());
}
barrier .await();
barrier .await();
long nsPerItem = timer.getTime() / ( nPairs * (long )nTrials );
System. out .println("Throughput: " + nsPerItem + " ns/item");
assertEquals(putSum.get(), takeSum.get() )
} catch (Exception e) {
throw new RuntimeException(e);
}
}


[align=left]
[/align]
[align=left]. 生产者消费者模式在不同参数组合下的吞吐率[/align]
[align=left]. 有界缓存在不同线程数量下的伸缩性[/align]
[align=left]. 如何选择缓存的大小[/align]

public static void main(String[] args) throws InterruptedException {
int tpt = 100000; // 每个线程中的测试次数
for (int cap = 1; cap <= tpt; cap *= 10){
System. out .println("Capacity: " + cap);
for (int pairs = 1; pairs <= 128; pairs *= 2){
TimedPutTakeTest t = new TimedPutTakeTest(cap, pairs, tpt);
System. out .println("Pairs: " + pairs + "\t");
t.test();
System. out .println("\t" );
Thread. sleep(1000);
t.test();
System. out .println();
Thread. sleep(1000);
}
}
pool .shutdown();
}


查看吞吐量/线程数量的关系

2 多种算法的比较

3 响应性衡量

三、避免性能测试的陷阱

1 垃圾回收

2 动态编译

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

4 不真实的竞争程度

5 无用代码的消除

四、其他的测试方法

1 代码审查

2 静态分析工具 
     FindBugs、Lint
3 面向方面的测试技术

4 分析与监测工具
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: