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


并发测试分为两类:安全性测试(无论错误的行为不会发生)而活性测试(会发生)。
安全測试 - 通常採用測试不变性条件的形式,即推断某个类的行为是否与其它规范保持一致。

活跃性測试 - 包含进展測试和无进展測试两个方面。

性能測试与活跃性測试相关,主要包含:吞吐量、响应性、可伸缩性。

一、正确性測试

找出须要检查的不变条件和后延条件。
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.test();
                        System. out .println("	" );
                        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 分析与监測工具






版权声明:本文博主原创文章,博客,未经同意不得转载。

原文地址:https://www.cnblogs.com/gcczhongduan/p/4850070.html