并发程序的测试
相比于串行程序,并发程序潜在的错误发生是随机的,并发测试分为
- 安全性测试:不发生任何错误的行为,通常测试不变性条件,即判断某个类的行为是否与其规范保持一致
- 活跃性测试:某个良好的行为终究会发生,包括进展测试和无进展测试
- 性能测试:吞吐量、响应性、可伸缩性
正确性测试
如下实现一个固定长度的队列,通过计数信号量实现可阻塞的put和take
- availableitems表示可以从缓存中删除的元素
- availableSpaces表示可以插入缓存的元素
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() {
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;
}
}
首先对其进行基本单元测试
@Test
public void testIsEmptyWhenConstruct() {
BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
assertTrue(bb.isEmpty());
assertFalse(bb.isFull());
}
@Test
public void testIsFullAfterPuts() throws InterruptedException {
BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
for (int i = 0; i < 10; i++) {
bb.put(i);
}
assertTrue(bb.isFull());
assertFalse(bb.isEmpty());
}
当测试阻塞操作时,需要在线程中启动阻塞操作,等到线程阻塞后再中断它,以此说明阻塞成功,如下子线程调用take从空缓存中获取元素会导致阻塞(若未阻塞则说明测试失败),随后中断并调用join回到主线程判断子线程是否存活
@Test
public void testTakeBlockWhenEmpty() {
BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
Thread taker = new Thread() {
public void run() {
try {
int unused = bb.take();
fail();
} catch (InterruptedException success) {
}
}
};
try {
taker.start();
Thread.sleep(2000);
taker.interrupt();
taker.join(2000);
assertFalse(taker.isAlive());
} catch (Exception e) {
fail();
}
}
不能使用Thread.getState来获取线程状态,阻塞线程可能不需要进入WAITING状态,而是自旋等待,而且切换状态需要时间,其获取的值并不准确,只能用于调试信息
安全性测试
想要测试并发类再并发访问的正确执行,需要使用多个线程执行并发操作,但测试程序本身就是并发
测试生产者-消费者模式有几种方式
- 加入影子列表,操作队列时并同时操作影子列表,然后测试完后判断影子列表的数据是否符合要求
- 通过对顺序敏感的校验和计算函数计算所有入列、出列元素校验和(校验和应该是随机的,且生成过程不能是同步的),并进行比较,多生产者-消费者模式应该使用顺序不敏感的校验和
public class PutTakeTest {
private final ExecutorService pool = Executors.newCachedThreadPool();
public final AtomicInteger putSum = new AtomicInteger(0);
public final AtomicInteger takeSum = new AtomicInteger(0);
private final CyclicBarrier barrier;
private final BoundedBuffer<Integer> bb;
private final int nTrials, nPairs;
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();//等所有线程执行完成
pool.shutdown();
} 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);
}
}
}
private int xorShift(int y) {
y ^= (y << 6);
y ^= (y >>> 21);
y ^= (y << 7);
return y;
}
}
@Test
public void main() {
PutTakeTest putTakeTest = new PutTakeTest(10, 10, 100000);
putTakeTest.test();
assertEquals(putTakeTest.putSum.get(),putTakeTest.takeSum.get());
}
对于如上程序
- xorShift()利用hashCode和nanoTime生成随机数
- CyclicBarrier数量为2n+1,当n个生产者和n个消费者线程同时运行到barrier.await()时,开启栅栏,从而产生更多的并发操作
- 栅栏再次启用,等待生产者和消费者线程处理完毕后对比putSum和takeSum是否一致,从而判断BoundedBuffer的并发功能是否正常
通过回调测试
public class TestThreadFactory implements ThreadFactory {
public final AtomicInteger numCreated = new AtomicInteger();
private final ThreadFactory mThreadFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r) {
numCreated.incrementAndGet();
return mThreadFactory.newThread(r);
}
}
如上,创建线程工厂记录线程数量,测试是否和线程的最大数量相等
@Test
public void testPoolExpansion() throws InterruptedException {
int MAX_SIZE = 10;
TestThreadFactory testThreadFactory = new TestThreadFactory();
ExecutorService executorService = Executors.newFixedThreadPool(MAX_SIZE, testThreadFactory);
for (int i = 0; i < 10 * MAX_SIZE; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
for (int i = 0; i < 20 && testThreadFactory.numCreated.get() < MAX_SIZE; i++) {
Thread.sleep(100);
}
assertEquals(testThreadFactory.numCreated.get(), MAX_SIZE);
executorService.shutdownNow();
}
性能测试
如下新增一个计时的Runnable,并传入CyclicBarrier
public class PutTakeTest {
private final ExecutorService pool = Executors.newCachedThreadPool();
public final AtomicInteger putSum = new AtomicInteger(0);
public final AtomicInteger takeSum = new AtomicInteger(0);
private final CyclicBarrier barrier;
private final BoundedBuffer<Integer> bb;
private final int nTrials, nPairs;
private final BarrierTimer timer;
class BarrierTimer implements Runnable {
private boolean started;
private long startTime, 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;
}
}
PutTakeTest(int capacity, int npairs, int ntrials) {
this.bb = new BoundedBuffer<Integer>(capacity);
this.nTrials = ntrials;
this.nPairs = npairs;
this.timer = new BarrierTimer();
this.barrier = new CyclicBarrier(npairs * 2 + 1, timer);
}
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(" time: " + nsPerItem + " ns/item");
pool.shutdown();
} 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);
}
}
}
private int xorShift(int y) {
y ^= (y << 6);
y ^= (y >>> 21);
y ^= (y << 7);
return y;
}
}
分别记录容量在1/10/100/1000时,线程数量在1/2/4/8/16/32/64/128时,计算次数为100000时,并发完成计算的性能
@Test
public void main() throws Exception {
int tpt = 100000;
for (int cap = 1; cap <= 1000; cap *= 10) {
System.out.println("cap = " + cap);
for (int pairs = 1; pairs <= 128; pairs *= 2) {
System.out.print("pairs = " + pairs);
PutTakeTest putTakeTest = new PutTakeTest(cap, pairs, tpt);
putTakeTest.test();
System.out.println("\t");
}
}
}
避免性能测试的陷阱
垃圾回收
垃圾回收可能在任何时刻执行导致运行时间差异,可采取的方法有
- 确保测试时不执行垃圾回收,可通过-verbose:gc判断是否执行了垃圾回收
- 确保垃圾回收在测试时执行多次,这个更贴近实际情况
动态编译
执行多次的代码会被编译成机器代码,从解释执行变成直接执行,会对运行时间产生影响,可采取的方法有
- 运行足够长的时间
- 预先运行一段时间,等代码完全编译后再开始测试,可通过-xx:+PrintCompilation判断动态编译是否完成
- 在同一个JVM运行多次,丢弃第一次测试结果(可能发生编译)
不真实的竞争程度
并发程序通过需要执行的操作有
- 访问共享数据
- 执行线程本地计算
它们是可以交替运行的,若本地计算时间越长,访问共享数据的开销就会显得越小,故在测试时应该模拟真实运行场景下的计算量
无用代码消除
测试程序通常不会执行任何计算,故容易被JVM消除,如上面PutTakeTest中计算的校验和最终没有使用的话,代码可能被消除
为避免代码消除,可以计算某个对象的hashcode并于任意数值比较
同时计算结果应该是不可预测的,否则编译器可能使用预先计算的结果代替计算过程
其他测试方法
- 代码审查
- 静态分析工具
- 分析与监测工具
1453

被折叠的 条评论
为什么被折叠?



