并发程序的测试:
- 测试并发程序最大的挑战在于:错误的发生并不具有确定性,而是随机的。
- 安全性测试和活跃性测试。
- 性能测试:即吞吐量,响应性,可伸缩性测试。
正确性测试:
- 测试一个有界缓存。
- 代码实现。
/**
* 基于信号量实现的有界缓存
*/
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(){
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; // let gc working
takePosition = (++i == items.length) ?0 : i;
return x;
}
}
/**
* 基于信号量实现的有界缓存
*/
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(){
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; // let gc working
takePosition = (++i == items.length) ?0 : i;
return x;
}
}
基本的单元测试:
- 例如上面BoundedBuffer的测试用例。
public class BoundedBufferTests {
@Test
public void testIsEmptyWhenConstructed(){
BoundedBuffer<String> bf = new BoundedBuffer<String>(10);
assertTrue(bf.isEmpty());
}
@Test
public void testIsFullAfterPuts() throws InterruptedException{
BoundedBuffer<String> bf = new BoundedBuffer<String>(10);
for (int i=0; i<10; i++){
bf.put("" + i);
}
assertTrue(bf.isFull());
assertTrue(bf.isEmpty());
}
}
public class BoundedBufferTests {
@Test
public void testIsEmptyWhenConstructed(){
BoundedBuffer<String> bf = new BoundedBuffer<String>(10);
assertTrue(bf.isEmpty());
}
@Test
public void testIsFullAfterPuts() throws InterruptedException{
BoundedBuffer<String> bf = new BoundedBuffer<String>(10);
for (int i=0; i<10; i++){
bf.put("" + i);
}
assertTrue(bf.isFull());
assertTrue(bf.isEmpty());
}
}
对阻塞操作的测试:
- 测试阻塞行为及对中断的响应性。
@Test
public void testTaskBlocksWhenEmpty(){
final BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
Thread taker = new Thread(){
@Override
public void run() {
try {
int unused = bb.take();
fail(); //不应执行到这里
} catch (InterruptedException e) {
}
}
};
try {
taker.start();
Thread.sleep(1000);
taker.interrupt();
taker.join(2000); //保证即使taker永久阻塞也能返回
assertFalse(taker.isAlive());
} catch (InterruptedException e) {
fail();
}
}
@Test
public void testTaskBlocksWhenEmpty(){
final BoundedBuffer<Integer> bb = new BoundedBuffer<>(10);
Thread taker = new Thread(){
@Override
public void run() {
try {
int unused = bb.take();
fail(); //不应执行到这里
} catch (InterruptedException e) {
}
}
};
try {
taker.start();
Thread.sleep(1000);
taker.interrupt();
taker.join(2000); //保证即使taker永久阻塞也能返回
assertFalse(taker.isAlive());
} catch (InterruptedException e) {
fail();
}
}
安全性测试:
- 在构建对并发类的安全性测试中,需要解决的关键性问题在于,要找出那些容易检查的属性,这些属性在发生错误的情况下极有可能失败,同时又不会使得错误检查代码人为地限制并发性。理想情况是,在测试属性中不需要任何同步机制。
/**
* 测试BoundedBuffer的生产者-消费者程序
*/
public class PutTakeTest {
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 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(); // 等待所有的线程执行完成
} catch (Exception e) {
throw new RuntimeException(e);
}
}
class Producer implements Runnable{
@Override
public void run() {
try {
int seed = 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);
}
}
}
..,
}
/**
* 测试BoundedBuffer的生产者-消费者程序
*/
public class PutTakeTest {
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 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(); // 等待所有的线程执行完成
} catch (Exception e) {
throw new RuntimeException(e);
}
}
class Producer implements Runnable{
@Override
public void run() {
try {
int seed = 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);
}
}
}
..,
}
资源管理的测试:
- 再由自己掌握资源分配的时候,必须主要对资源的回收操作。
void testLeak() throws InterruptedException{
BoundedBuffer<Big> bb = new BoundedBuffer<Big>(CAPACITY);
// 生成堆快照
for (int i=0; i<CAPACITY; i++){
bb.put(new Big());
}
for (int i=0; i<CAPACITY; i++){
bb.take(); //这里必须会通过doExtract(): items[i] = null;来释放资源
}
// 生成堆快照, 应该和上一个快照内存容量基本相同
}
class Big {
double[] data = new double[100000];
}
void testLeak() throws InterruptedException{
BoundedBuffer<Big> bb = new BoundedBuffer<Big>(CAPACITY);
// 生成堆快照
for (int i=0; i<CAPACITY; i++){
bb.put(new Big());
}
for (int i=0; i<CAPACITY; i++){
bb.take(); //这里必须会通过doExtract(): items[i] = null;来释放资源
}
// 生成堆快照, 应该和上一个快照内存容量基本相同
}
class Big {
double[] data = new double[100000];
}
使用回调:
- 我们可以通过自定义扩展类来进行相关测试。
public class TestingThreadFactory implements ThreadFactory {
public final AtomicInteger numCreated =
new AtomicInteger(); //记录创建的线程数
private final ThreadFactory factory =
Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r) {
numCreated.incrementAndGet();
return factory.newThread(r);
}
}
public class TestingThreadFactory implements ThreadFactory {
public final AtomicInteger numCreated =
new AtomicInteger(); //记录创建的线程数
private final ThreadFactory factory =
Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r) {
numCreated.incrementAndGet();
return factory.newThread(r);
}
}
/**
* 验证线程扩展能力
* @throws InterruptedException
*/
public void testPoolExpansion() throws InterruptedException{
int MAX_SIZE = 10;
TestingThreadFactory threadFactory = new TestingThreadFactory();
ExecutorService exec = Executors.newFixedThreadPool(MAX_SIZE, threadFactory);
for (int i=0; i<10*MAX_SIZE; i++){
exec.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 &&
threadFactory.numCreated.get() < MAX_SIZE; i++){
Thread.sleep(100);
}
assertEquals(threadFactory.numCreated.get(), MAX_SIZE);
exec.shutdownNow();
}
/**
* 验证线程扩展能力
* @throws InterruptedException
*/
public void testPoolExpansion() throws InterruptedException{
int MAX_SIZE = 10;
TestingThreadFactory threadFactory = new TestingThreadFactory();
ExecutorService exec = Executors.newFixedThreadPool(MAX_SIZE, threadFactory);
for (int i=0; i<10*MAX_SIZE; i++){
exec.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 &&
threadFactory.numCreated.get() < MAX_SIZE; i++){
Thread.sleep(100);
}
assertEquals(threadFactory.numCreated.get(), MAX_SIZE);
exec.shutdownNow();
}
产生更多的交替操作:
- 可使用Thread.yield()产生更多的上下文切换(与平台有关)。
- Thread.sleep(shortTime)进行短时间睡眠。
public synchronized void transferCredits(Account from, Account to, int amount){
from.setBalance(from.getBalance() - amount);
if (random.nextInt(1000) > THRESHOLD){
Thread.yield();
}
to.setBalance(to.getBalance() + amount);
}
public synchronized void transferCredits(Account from, Account to, int amount){
from.setBalance(from.getBalance() - amount);
if (random.nextInt(1000) > THRESHOLD){
Thread.yield();
}
to.setBalance(to.getBalance() + amount);
}
性能测试:
- 性能测试的目标:
端到端性能。
调整各种不同的限值,如线程数量,缓存容量等。
- 对PutTakeTest进行定时扩展:
this.timer = new BarrierTimer();
this.barrier = new CyclicBarrier(npairs * 2 + 1, timer);
/**
* 基于栅栏的定时器
*/
public 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 long getTime(){
return endTime - startTime;
}
public synchronized void clear(){
started = false;
}
}
this.timer = new BarrierTimer();
this.barrier = new CyclicBarrier(npairs * 2 + 1, timer);
/**
* 基于栅栏的定时器
*/
public 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 long getTime(){
return endTime - startTime;
}
public synchronized void clear(){
started = false;
}
}
其测试方法:
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();
System.out.println("cost time: " + nsPerItem);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
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();
System.out.println("cost time: " + nsPerItem);
} 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.test();
Thread.sleep(1000);
t.test();
Thread.sleep(1000);
}
}
}
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.test();
Thread.sleep(1000);
t.test();
Thread.sleep(1000);
}
}
}
多种算法的比较:
- BoundedBuffer, ArrayBlockingQueue, ListBlockingQueue比较
响应性衡量:
- 除非线程由于密集的同步需求而被持续的阻塞,否则非公平的信号量通常能实现更好的吞吐量,而公平的信号量则实现更低的变动性。
- 公平信号量的开销主要来自线程阻塞造成,通过单元素缓存来进行类似测试:
避免性能测试的陷阱
垃圾回收:
- 两种防止垃圾回收对测试结果产生偏差的策略:
垃圾回收在执行测试程序期间不被执行,可通过-verbose:gc查看垃圾回收信息。
垃圾回收在执行测试程序期间执行多次,可以充分反映出运行期间的内存分配和垃圾回收等开销。
动态编译:
- 可以让测试程序运行足够长时间,防止动态编译对测试结果产生的偏差。
对代码路径的不真实采样:
- 测试程序不仅要大致判断某个典型应用程序的使用模式,还要尽量覆盖在该应用程序中将执行的代码路径集合。
不真实的竞争程度:
- 并发应用可以交替执行两种不同类型的工作:访问共享数据(如线程池的工作队列)和执行线程本地的计算。
无用代码的消除:
- 优化编译器能找出并消除那些不会对输出结果产生任何影响的无用代码(Dead Code), 不要留提醒。
- 要编写有效的性能测试程序,就需要告诉优化器不要将基准测试当作无用代码而优化掉,这就要求在程序中对每个计算结果都要通过某种方式来是使用,这种方式不需要或者大量的计算。
其他的测试方法
代码审查;
- 花一定时间让其他人来审查代码总是物有所值的(Code Review)。
静态分析工具:
- FindBugs, CheckStyle等。
面向方面的测试技术:
- AOP保证某些不变性条件不被破坏,或者与同步策略的某些方面保持一致。
分析与监测工具:
- 通过ThreadInfo类或JMX代理可以监控一些线程信息。
不吝指正。