并发测试分为两类:安全性测试(无论错误的行为不会发生)而活性测试(会发生)。
安全測试 - 通常採用測试不变性条件的形式,即推断某个类的行为是否与其它规范保持一致。
活跃性測试 - 包含进展測试和无进展測试两个方面。
性能測试与活跃性測试相关,主要包含:吞吐量、响应性、可伸缩性。
一、正确性測试
找出须要检查的不变条件和后延条件。
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 安全性測试
在构建对并发类的安全性測试中,须要解决地关键性问题在于,要找出那些easy检查的属性,这些属性在错误发生的情况下极有可能失败,同一时候又不会使得错误检查代码人为地限制并发性。理想情况是,在測试属性中不须要不论什么同步机制。
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); } }
. 生产者消费者模式在不同參数组合下的吞吐率
. 有界缓存在不同线程数量下的伸缩性
. 怎样选择缓存的大小
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 分析与监測工具
版权声明:本文博主原创文章,博客,未经同意不得转载。