测试并发程序
并发程序在设计上采用与顺序程序大致相同的原则和模式。不同的是并发程序存在一定程度的不确定性。
类似地,很多用来测试并发程序的方法,都借鉴并发展了测试顺序程序的方法。一些用于测试顺序程序正确性与性能的技术,同样可以用在测试并发程序上。
为并发程序创建测试,所要面临的主要挑战在于:那些潜在错误的发生并不具有确定性,而是随机的;能够揭示这种失败的测试,与普通的顺序测试相比,一定要有更广泛的覆盖度和更长的运行时间。
并发类的测试基本分为两类:对安全性测试、对活跃度测试。
安全性测试通常会验证不变约束,以这种方式来检查类的行为是否遵循了它的规约。
活跃度特性自身。就是对测试的一大挑战,活跃度测试包括“应该执行”和“不该执行”两个方面。这些都是很难量化的。
与活跃度测试相关的是性能测试。
性能可以通过很多方式来测量:
吞吐量
- 在一个并发任务集里,已完成任务所占的比例
响应性
- 从请求到完成一些动作之间的延迟。(等待时间)
可伸缩性
- 增加更多的资源,就能提高吞吐量。
10.1测试正确性
为并发类开发单元测试的流程,始于和顺序类相同的分析——识别出不变约束和后验条件。
示例:
//利用Semaphote实现有限缓存、@ThreadSafepublic class BoundedBuffer<E>{private final Semaphore availableItms, availableSpaces;@GuardedBy("this")private final E[] tiems;@GuardedBy("this")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 voolean isFull(){return availableSpaces.availablePermits() == 0;}public void put(E x) throws InterruptedException{availableSpaces.acquire();doInsert(x);availableItems.release();}private 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];takePosition = (++i == items.length) ? 0 : i;return x;}}
10.1.1基本的单元测试
示例:
//BounderBuffer的基本单元测试//先创建一个有限缓存,再调用它的方法,最后断言它的后验条件和不变约束。class BoundedBufferTest extends TestCase{//一个新建立的缓存应该确定自己是空的,同时不是满的void testIsEmptyWhenConstructed(){BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10);assertTrue(bb.isEmpty());assertFalse(bb.isFull());}//测试缓存是否意识到自己已经满了void testIsFullAfterPuts() throws InterruptedException{BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10);for(int i = 0; i < 10; i++){bb.put(i);}assertTrue(bb.isFull());assertFalse(bb.isEmpty());}}
这些简单的测试方法完全是顺序化的。在你的测试套件中包含一个顺序化的测试集,这种做法通常是有帮助的,这些测试集可能在错误还没有涉及并发问题时就会发现它们。
10.1.2测试阻塞操作
示例:
//这个测试方法测试了take的集中特性:阻塞、被中断后抛出InterruptedException
void testTakeBlockesWhenEmpty(){
final BoundedBuffer<Integer> bb = new BoundedBuffer<Integer>(10);
Thread taker = new Thread(){
public void run(){
try{
//从空缓存中获取元素,所以会阻塞
int unused = bb.take();
fail();//如果运行到这里,说明有问题
}catch(InterruptedException success){}
}
};
try{
taker.start();
//主线程睡眠,确保子线程先执行
Thread.sleep(LOCKUP_DETECT_TIMEOUT);
//中断taker线程,让它抛出异常InterruptedException
taker.interrupt();
//在一定时间里等待taker线程(防止出现意外情况)
taker.join(LOCKUP_DETECT_TIMEOUT);
//判断taker线程是否存活
assertFalse(taker.isAlive());
}catch(Exception unexpected){
fail();
}
}
只有在极少数情况下,显示地子类化Thread会比在池中使用Runnable更合适:
为了使用join来测试正确的终止。
同样的方法,还可以测试主线程将一个元素放入队列后,是否会解除taker线程的阻塞。
10.1.3测试安全性
之前只是测试了有限混村的重要属性,不会发现数据竞争引发的错误。
想要测试一个并发类在不可预知的并发访问下是否能够正确执行,我们需要安排多个线程运行put和take操作。
为并发类创建有效的安全测试,其挑战在于:
如何在程序出现问题并导致某些属性极度可能失败时,简单地识别出这些受检查的属性来。
同时不要人为的让查找错误的代码限制住程序的并发性。
最好能做到在检查测试的属性时,不需要任何的同步。
方法1:
使用一个**对顺序敏感的检验求和函数**,计算出**入队与出队的元素个数**,然后进行**比较**,如果它们相等,那么测试通过。
在**只有一个生产者**向缓存置入元素,同时**只有一个消费者**取出元素的情况下,这种方法不仅可以**测试是否取出了正确的元素**,而且还能**测试元素被取出的顺序是否正确**。
因此,**当只有唯一的生产者和消费者时,这种方法效果很好**。
扩展方法1:
如果想让它用于多生产者、多消费者的情况,就需要使用一个不同的检验求和函数,它对元素入队的顺序是不敏感的,这样可以在测试完成后,再将多个核查结果组合在一起。
为了保证你的测试的确测试了你所想的事情,有一点很重要:不能让编译器预先得知checksume的值。所以我们不能使用恒定的正数作为测试数据,应该使用随机数。
但是大多数随机数生成器类都是线程安全的,因此会引入额外的同步。
伪随机示例:
static int xorShift(int y){
y ^= (y << 6);
y ^= (y >>> 21);
y ^= (y << 7);
return y;
}
CyclicBarrier:
循环栅栏,它的作用就是会让所有线程都等待完成后才会继续下一步行动。
构造方法:
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
//parties 是参与线程的个数(冲破栅栏所需要的线程个数)
//barrierAction 最后一个到达线程要做的任务
主要方法:
public int await() throws InterruptedException,BrokenBarrierException
public int await(long timeout,TimeUtil unit) throws .....
//线程调用await()表示自己已经到达栅栏
//BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
示例:
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 static void main(String[] args){
new PutTakeTest(10, 10, 10000).test();
pool.shutdown();
}
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++){
//启动了N个生产者线程同时生成元素
pool.execute(new Producer());
//启动了N个消费者线程从队列取出元素
pool.execute(new Consumer());
}
barrier.await();//等待所有线程做好准备(所有线程准备好时,冲破栅栏一次)
barrier.await();//等待所有线程最终完成(所有线程都运行完成时,冲破栅栏一次)
//比较入队次数和出队次数是否一致。
assertEquals(putSum.get, takeSum.get());
}catch(Exception e){
throw new RuntimeException(e);
}
}
class Producer implements Runnable {
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 {
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);
}
}
}
}
测试应该发生在多处理系统上运行,以提高潜在交替运行的多样性。
但是,多个CPU未必会使测试更加高效。
为了能够最大程度地检测到时序敏感的数据竞争的发生机会,应该让测试中的线程数多于CPU数,这样在任何给定的时间里,都有一些线程在运行,一些被交换出执行队列,这样可以增加线程间交替行为的随机性。
10.1.4测试资源管理
之前的测试都是为了测试类有没有做到它应该做的。
测试的另一个方面是测试类没有做它不应该做的,比如资源泄露。
任何持有或管理着其他对象的对象,都应该在不需要某个对象时,放弃对该对象的引用。
限制缓存,就是为了防止资源耗尽导致应用程序失败的发生,对缓存的限制,会引起生产力过剩的生产者阻塞,这样它们就不会持续创建更多的工作。
对内存不合理的占有,可以简单地通过堆检查工具测试出来,这些工具可以测量应用程序内存的使用。
示例:
class Big {double[] data = new double[10000];}
//testLeak方法会向一个有限缓存中插入几个巨型对象,然后将它们移除,2号堆快照的内存用量应该大致等于1号堆快照的内存用量。
void testLeak() throws InterruptedException{
BoundedBuffer<Big> bb = new BoundedBuffer<Big>(CAPACITY);
//这里会进行一次垃圾回收。然后记录下堆大小和内存用量的信息
int heapSize1 = /*heap的快照*/;
for(int i = 0; i < CAPACITY; i++){
bb.put(new Big());
}
for(int i = 0; i < CAPACITY; i++){
bb.take();
}
int heapSize2 = /*heap的快照*/;
assertTrue(Math.abs(heapSize1 - heapSize2) < THRESHOLD);
}
10.1.5使用回调
回调用户提供的代码,有助于创建测试用例,回调常常发生在一个对象生命周期的已知点上。例如ThreadPoolExecutor就把调用转到了任务的Runnable和ThreadFactory上。
示例:
//用于测试ThreadPoolExecutor的线程工厂
//这个类维护了已创建线程的数目,在测试运行期间,测试用例可以验证已经创建的数量
class TestingThreadFactory implements ThreadFactory{
public final AtomicInteger numCreated = new AtomicInteger();
private final ThreadFactory factory = Executors.defaultThreadFactory();
public Thread newThread(Runnable r){
newCreated.incrementAndGet();
return factory.newThread(r);
}
}
//验证线程池扩展的测试方法
//如果核心池大小小于最大值,线程池会在执行的任务增多时相应的增长。
//向池提交几个耗时任务,会使得池中的执行任务的数量在足够长的时间内都是常量,这样就可以进行一些断言,比如测试池是否如期地扩展。
public void testPoolExpansion() throws InterruptedException{
int MAX_SIZE = 10;
ExecutorService exec = Executors.newFixedThreadPool(MAX_SIZE);
for(int i = 0; i < 10 * MAX_SIZE; i++){
exec.executr(new Runnable(){
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_VALUE);
exec.shutdownNow();
}
10.1.6产生更多的交替操作
因为大多数并发代码潜在的错误都是低可能性的事件,所以测试并发错误就要千篇一律的重复。
不过有些事情可能提高你发现错误的几率,在多处理器系统上,如果处理器的数量少于活动线程的数量,那么就可以产生更多的交替行为。
有一个有用的技巧可以提高交替操作的数量,那就是在访问共享状态的操作期间,使用Thread.yield激发更多的上下文转换。
提示:Thread.yield是与平台相关的,因为JVM只是简单地把它当作一个no-op:使用短暂而非零的sleep。
示例:
//有时在一个操作之间进行yield,你可以在那些没有使用充足同步去访问状态的代码中,激活时序敏感的bug。
public synchronized void transferCredits(Account from,
Account to,
int amount){
from.setBalance(from.getBalace() - amount);
if(random.nextInt(1000) > THRESHOLD){
Thread.yield();
}
to.setBalance(to.getBalace() + amount);
}
10.2测试性能
性能测试通常是功能测试的延伸。
事实上,在性能测试中需要包含一些基本的功能测试,这样就能确保你正在做性能测试的代码是正确的。
性能测试:
要探寻具有代表性的用里从头至尾的性能标准。
- 但是获得被测试对象的合理使用场景并不容易。某些情况下,合理的测试场景容易被发现。比如:几乎所有的生产者-消费者都会用到有限缓存,所以要去测量生产者给消费者供应数据的吞吐量。
为那些基于经验的不同界限选择一个合适的大小(线程数、缓存容量等等)
- 这些数值可能会非常依赖于平台的特征,处理器的数量,或者内存的大小,需要动态地配置,为它们选择一个合理的值,可以广泛地应用在各个系统中。
10.2.1扩展PutTakeTest,加入事件特性
与其测量一个单一操作的耗时,不如选择对整个运行计时,然后除以操作的数量,得到每个操作的耗时,这样可以获得更精确测值。
示例:
public class BarrierTimer implements Runnable {
private boolean started;
private long startTime, endTime;
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;
}
}
public void test(){
try{
time.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.print("Throughput:" + nsperItem + "ns/item");
assertEquals(putSum.get(), takeSum.get());
}catch(Exception e){
throw new RuntimeException(e);
}
}
//有时候,添加更多的线程只会引起性能的轻微下降。因为即使有很多的线程,却没有更多的计算让它们去执行,大多数CPU时间都花在线程的阻塞和解除阻塞上面了。
//又因为更多的线程在做相同的事情,所以大量CPU都处于闲置状态,不会过多影响性能。
public static void main(String[] args) throws Exception{
int tpt = 100000;//每个线程尝试的次数
for(int cap = 1; cap <= 1000; cap *= 10){
System.out.println("Capacity:" + cap);
for(int pairs = 1; pairs <= 128; pairs *= 2){
TimedPutTaskTest t = new TimedPutTakeTest(cap, pairs, tpt);
System.out.print("Pairs:"+ pairs + "\t");
t.test();
System.out.print("\t");
THread.sleep(1000);
t.test();
System.out.print();
Thread.sleep(1000);
}
}
pool.shutdown();
}
在测试中,生产者不费什么理器就能生产一个条目,消费者也不需要什么力气就能获取一个条目。
但是,在真实的生产者-消费者应用程序中,如果工作者线程要通过一些复杂的工作来生产和消费条目,那么前面那种CPU的闲置状态就会消失,多线程所产生的效果就会变得明显起来。
这个测试的主要目的:测量在生产者和消费者通过有限缓存交换数据时,哪个约束条件影响了总体的吞吐量。
10.2.2比较多种算法
虽然BoundedBuffer是一种可靠的实现,但是它还不足以和ArrayBlockingQueue与Linked’BlockingQueue相提并论。Java.util.concurrent中的算法已经被选择并调整到我们已知的最佳性能状态。
BoundedBuffer的运行效率不高,主要原因:put和take操作分别都有多个操作可能遇到竞争——获得一个信号量,获得一个锁,释放信号量。
经过测试可以发现,在多线程核心的机器上,LinkedBlockingQueue的伸缩性好于ArrayBlockingQueue。因为链接队列的put和take操作允许有比基于数组队列更好的并发访问。最佳的链接队列算法允许队列的头和尾彼此对立地更新。
算法通过多做一些分配操作,可以降低竞争,这样的算法通常具有更好的可扩展性。
10.2.3测试响应性
吞吐量:是并发程序里最重要的性能指标。
但也有时候,知道一个独立的动作完成要花费多少时间,也是非常重要的。这种情况下,我们要测量的是服务时间的差异性。
有时,为了能获得较小的服务时间差异性,那么允许较长时间的平均服务时间是有意义的。
除非线程总是由于密集的同步条件而持续地被阻塞,非公平的信号量通常能够提供更好的吞吐量,公平的信号量提供更低的差异性 。因为结果差异如此之大,所以Semaphore强迫它的客户来决定针对哪一个特性进行优化。
10.3避免性能测试的陷阱
理论上,开发性能测试是简单的。实际中,还必须要提防很多编码的陷阱,它们会导致性能测试产生毫无意义的结果。
10.3.1垃圾回收
垃圾回收的时序是不可预知的,在一个测试数据的测试运行中,任何时候垃圾回收器都有可能运行。
如果一个测试程序反复执行了N次都没有触发垃圾回收,但是在第N+1次的时候出发了一次垃圾回收,运行中大小差不多,但是运行时间却差很多。
两种方式避免垃圾回收造成的影响:
确保在测试运行的整个期间,垃圾回收根本不会执行
- 通过调用JVM时使用-verbose:gc 可以做到
确保执行测试期间垃圾回收器运行多次
- 这样测试程序能够充分反映出运行期间的分配与垃圾回收的开销。(这种更好)
大多数基于生产者-消费者设计的应用程序都会涉及相当数量的内存分配与垃圾回收的操作:生产者分配新对象,消费者使用并丢弃。运行有限缓存测试的时间足够长,就会引发多次垃圾回收,从而得到更加精确的结果。
10.3.2动态编译
对于向Java这样的动态编译语言,编写和解读它们的性能基准测试,要比C或C++这样的静态编译语言困难得多。
现在最新的JVM结合了字节码解释和动态编译:
当一个类被**首次加载**后,JVM会以**解释字节码的方式执行**,如果一个方法**运行得足够频繁**,**动态编译器最终会将它挑出来**,**转换成本机代码**,当编译完成后,执行方式将由**解释执行转换到直接执行**。
编译的时机是不可预知的,你的**时序测试应该在所有代码编译完成后再运行**:**测量解释执行的代码是没有意义的**,因为频繁执行的代码路径都会被编译。(而且编译也会消耗CPU)
由于很多原因,代码同样可能被**解除编译(回复到解释型执行)以及再次重编译**:比如加载了一个类,会破坏以前的编译结果,而在收集到足够信息后,再重编译一个代码路径。
再HotSpot中,运行程序时使用**-xx:+PrintCompilation**,那么程序会**在动态编译运行时打印出信息**。
10.3.3代码路径的非真实取样
**运行时编译器**使用收集到的信息老帮助**优化已经编译的代码**。
JVM被允许使用**执行期间的细则信息**以此产生更好的代码,这意味着在一个程序中编译方法M生产的代码可能与另一个不同。
在某种情况下,JVM可以进行基于下述假设的优化:
**这种优化只是临时的,过后,当他们不适合动态编译模式后,JVM会使被编译的代码无效,从而把它们回收**。
作为结果,你的测试程序不仅应该尽量地接近于一个典型应用程序的使用模式,还应该**尽量覆盖这个应用程序会用到的代码路径的集合**,否则,动态编译器会针对一个存粹单线程化的测试程序,进行一些专门的优化。只要一个真实的应用程序中包含了一点偶发的并行,这种优化就不会出现在该程序中。
**因此,即使只是想测量单线程的性能,也应该与多线程的性能测试结合在一起**。
10.3.4不切实际的竞争程度
并发的应用程序总是交替执行两种非常不同的工作:
- 访问共享数据
- 线程本地的计算(假设任务自身并不访问共享数据)
依赖于两种工作类型的相关特性,应用程序会经历不同级别的竞争,并表现出不同的性能与伸缩性行为。
如果任务都是计算密集型的、耗时的(但并未频繁地跨线程访问数据),这种情况几乎没有竞争,吞吐量只受限于可用的CPU资源。
如果任务的生命周期很短,在工作队列上就会存在大量竞争,此时吞吐量受限于同步的开销。
为了让测试更有实际意义,应该尽量去模拟让线程本地的计算由某一个特有的应用程序去完成。
10.3.5死代码的消除
**优化过的编译器擅长发现并遗弃死代码**,这些代码不会对结果产生任何影响。
由于基准测试通常不会进行任何计算,它们就称为了这类优化的目标。
很多微型的基准测试在HotSpot的-server编译模式下的运行效果,要好于-client模式,这不仅是因为server模式的编译器可以产生更有效的代码,同时还因为这种模式更擅长优化死代码。
**编写有效的性能测试,就需要哄骗优化器不要把你的基准测试当作死代码而优化掉。这需要每一个计算的结果都要应用在你的程序中——以一种不需要的同步或真实计算的方式**。
防止优化小技巧:
//我们希望不要优化掉System.nanoTime的值
if(foo.x.hashCode() == System.nanoTime()){
System.out.print(" ");
}
//这个比较很少能成功,如果成功了,也只是输出一个空字符,但是却用了System,nanoTime,防止它被优化掉
10.4测试方法补遗
在复杂的程序中,再多的测试也无法发现所有错误。
测试的目标不是更多地发现错误,而是提高信心。因为设想发现所有bug是不现实的。
所以质量审查(QA)计划的目标应该定为利用现有的测试资源,最大程度上获得对代码的信心。
不同的QA方法,在发现某些类型的错误时更有效,而对于某些错误则无效。使用一些补充的测试方法,比如代码审查、静态分析……
10.4.1代码审查
就像单元测试和压力测试对于发现并发bug的有效性和重要性一样,没有什么可以取代严格的多人代码审查。(代码审查也不能取代测试)
10.4.2静态分析工具
静态代码分析:**不执行代码,对它进行分析**。
代码何查工具可以分析类,需要常见的错误模式的示例。像开源的FindBugs这种静态分析工具,都包含了很多常见代码错误的错误模式侦测器。
静态分析工具会生成一个警告清单,它必须被手工地检查,以确定这些警告是否代表了一个真正的错误。
FindBugs包含的并发相关的错误模式的侦测器:
不一致的同步性。
- 如果一个对象使用内部锁保护所有变量,然后频繁访问这个域的线程并未总是持有this锁,这就可能暗示同步策略没有被一贯的坚持。
调用Thread.run。
- 不能直接调用线程中的run方法,应该是 Thread.start。
未释放的锁
- 不同于内部锁,显式锁在控制退出了它们被请求的范围时,不会自动释放。
空synchronized块
- 空synchronized块在Java存储模型下是有语义的。
双检查锁
- 在惰性初始化时,双检查锁作为降低同步开销的技巧是有缺陷的。
从构造函数中启动线程
- 在构造函数中启动线程,会引入子类化问题的风险。同时还会引起this引用从构造函数中逸出。
通知错误
- notify和motifyAll方法预示着,一个对象的状态可能已经以某种方式发生了改变,进而那些正在等待与该对象相关联的条件队列的线程,会被解除阻塞。如果在一个synchronized块中调用了notify和notifyAll,但是没有修改任何状态,这通常都可能是个错误。
条件等待错误
- 调用Object.wait和Condition.wait时,不持有锁。
误用Lock和Condition。
- 使用Lock作为synchronized块的锁代替品,就像查找/替换一样,只要将wait调用替换成Condition.await调用。
休眠或等待时持有锁。
- 调用Thread.sleep时持有锁。会导致其他线程在很长的一段时间内无法执行。
自旋循环
- 如果代码除了循坏检查一个域是否有期望值之外,不做任何事情,就会浪费CPU时间,并且如果域不是volatile类型的,就不发保证循环检查可以终止。
10.4.3统计与剖析工具
大多数商业的统计工具都提供了一些对线程的支持。这会改变程序的特征集和执行效果,但是通常提供了对于程序正在做的事情的一个深入的洞察力。
内置的JMX代理也为监控线程行为提供了有限的特性。**ThreadInfo类就包含了线程的当前状态,而且当线程被阻塞时,它还包含引起阻塞的锁或者条件队列的信息**。
**如果激活了“线程竞争监视器”特性(由于对性能的影响,这个属性默认是关闭的)ThreadInfo还会包括很多线程阻塞等待一个锁或通知的时间,以及它花费在等待上累计的时间**。
总结
并发程序很多可能的失败模式都是低可能性的事件,它们很容易受到时序、加载和其他一些难以再现的条件的影响。
而且,在测试基础架构时,还会引入额外的同步或者分时的约束,这些会屏蔽被测试代码中的并发问题。
Java还有动态编译、垃圾回收以及自动的优化,都会影响对时间的测量。
所以,应该在运用传统的测试技术的同时,结合代码审查和自动化分析工具。
