《java并发编程实战》读书笔记9--并发程序的测试

第12章 并发程序的测试

大致分为两类:安全性测试和活跃性测试

12.1 正确性测试

找出需要检查的不变性条件和后验条件。接下来将构建一组测试用例来测试一个有界缓存。程序清单12-1给出了BoundedBuffer的实现,其中使用Semaphore来实现缓存的有界属性和阻塞行为。

BoundedBuffer实现了一个固定长度的队列,其中定义了可阻塞的put和take方法,并通过两个计数信号量进行控制。(实际情况中如果需要一个有界缓存,应该直接使用ArrayBlockingQueue或者LinkedBlockingDeque,而不是自己编写)。

12.1.1 基本的单元测试

BoundedBuffer的最基本单元测试类似与在串行上下文中执行的测试。首先创建一个有界缓存,然后调用它的各个方法,并验证它的后验条件和不变性条件。我们很快会想到一些不变性条件:新建立的缓存应该是空的,而不是满的。另一个略显复杂的安全测试是,将N个元素插入到容量为N的缓存中,然后测试缓存是否已经填满。

12.1.2 对阻塞操作的测试

在测试并发的基本属性时,需要引入多个线程。大多数测试框架并不能很好地支持并发性测试。在java.util.concurrent的一致性测试中,一定要将各种故障与特定的测试明确地关联起来。因此JSR 166专家组创建了一个基类,其中定义了一些可以在tearDown期间(主要实现测试完成后的垃圾回收等工作)传递和报告失败信息,并遵循一个约定:每个测试必须等待它锁创建的全部线程结束以后才能完成。

如果某方法需要在某些特定条件下阻塞,那么当测试这种行为时,只有当线程不再继续执行时,测试才是成功的。要测试一个方法的阻塞行为,类似于测试一个抛出异常的方法:如果这个方法可以正常返回,那么就意味着测试失败。在测试方法的阻塞行为时,将引入额外的复杂性:当方法被成功阻塞后,还必须使用方法接触阻塞。实现这个功能的一种简单方式就是使用中断——在一个单独的线程中启动一个阻塞操作。来个例子:

看了后面忘了前面。。。 现在补下之前的一些知识:

(1) 中断是通过调用Thread.interrupt()方法来做的. 这个方法通过修改了被调用线程的中断状态来告知那个线程, 说它被中断了. 对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true; 对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为false.

(2) t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程

如果take操作由于某种意料之外的原因停滞了,那么支持时限的join方法能够确保测试最终完成。用于验证线程能否在一个条件等待上阻塞的Thread.getState方法并不可靠(详见p208)

12.1.3 安全性测试

上面的几个测试程序无法发现由于数据竞争而引发的错误。要测试在生产者——消费者模式中使用的类,一种有效的方法就是检查被放入队列中和从队列中取出的各个元素。可以通过一个对顺序敏感的校验和计算函数里计算所有入列元素和出列元素的校验和,并进行比较。如果二者相等那么测试就死成功的。如果要将这种方法扩展到多生产者-多消费者模式的情况,就需要对元素入列/出列顺序不敏感的校验和函数,从而在测试程序运行完后,可以将多个校验和以不同的顺序组合起来。(这部分看得各种懵比)

当线程到达栅栏位置时调用栅栏的await方法,这个方法阻塞直到所有线程都达到栅栏的位置。如果所有线程都达到了栅栏的位置,那么栅栏将被打开,所有线程被释放,栅栏将被重置以便下次使用。

12.1.4 资源管理的测试(p212)

12.1.5 使用回调(p213) 

12.1.6 产生更多的交替操作(p214)

由于并发代码中的大多数错误都是一些低概率时间,因此在测试并发错误时需要反复地执行多次,所以有时候需要产生更多的交替操作。一种有用的方法是:在访问共享状态的操作中,使用Thread.yield将产生更多的上下文切换。

yield方法会使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

应该是面向切面的编程好伐,再次吐槽下这本书的翻译质量。

12.2 性能测试(p215)

12.2.1 在PutTakeTest中增加计时功能(p215)

记录整个运行过程的时间,然后除以操作数的量,从而得到每次操作的运行时间。使用一个栅栏动作来测量启动和结束时间:

老实说这部分我看到云里雾里的,不知道是不是翻译的原因(摔锅中...),二周目准备把这本书的英文版拿来看一遍。

12.2.2 多种算法的比较(p217)

12.2.3 响应性衡量(p219)

 

12.3 避免性能测试的陷阱(p220)

12.3.1 垃圾回收(p220)

垃圾回收的执行时序是无法预测的,因此在执行测试时,垃圾回收器可能在任何时刻运行。

12.3.2 动态编译(p220)

12.3.3 对代码路劲的不真实采样(p222)

12.3.4 不真实的竞争程度(p222)

12.3.5 无用代码的消除(p223)

12.4 其他的测试方法(p224)



总结:这章基本到后面就看不懂了...., 说了很多理论性的东西。其实我一直觉得理论这东西光说没用,要在实践的基础上理解才行。比较推崇先实践后理论的教学方式,而不是先从理论开始。这里再吐槽下这本书的翻译质量,坑啊。

时间: 2024-12-26 12:21:39

《java并发编程实战》读书笔记9--并发程序的测试的相关文章

JAVA并发编程实战 读书笔记(二)对象的共享

<java并发编程实战>读书摘要 birdhack 2015年1月2日 对象的共享 JAVA并发编程实战读书笔记 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关键之synchronized只能用于实现原子性或者确定临界区.同步还有另一个重要的方面:内存可见性. 1.可见性 为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 在没有同步的情况下,编译器.处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整.在缺乏足够同步的多线程程

JAVA并发编程实战 读书笔记(一)线程安全性

线程安全性   1.什么是线程安全 在线程安全的定义中,最核心的概念是正确性.正确性的含义是,某个类的行为与规范完全一致.当对正确性有了一个比较清晰的定义后,就可以定义线程安全性:当多个线程访问某个类时,这个类始终能表现出正确的行为,那这个类就是线程安全的. 举例:无状态对象一定是线程安全的. 大多数Servlet都是无状态的,当Servlet在处理请求时需要保存一些信息时,线程安全才会成为一个问题. 2.原子性 举个例子:语句 ++i:虽然递增操作++i是一种紧凑的语法,使其看上去是一个操作,

《java并发编程实战》笔记(一)

最近在看<java并发编程实战>,希望自己有毅力把它读完. 线程本身有很多优势,比如可以发挥多处理器的强大能力.建模更加简单.简化异步事件的处理.使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题.看下面例子: public class UnsafeSequence { private int value; /*返回一个唯一的数值*/ public int getNext(){ return value++; } } UnsafeSequence的问题在于,如果执行时机不对,那么

Java并发编程实践读书笔记--第一部分 基础知识

目前关于线程安全性没有一个统一的定义,作者自己总结了一个定义,如下:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的. 在并发编程中,由于不恰当的执行时序而出现不确定的结果的情况被称为竞态条件(Race Condition).最常见的竞态条件就是“先检查后执行(Check-Then-Act)”操作,即通过一个可能已经失效的观察来决定下一步的动作.比较简单的例子就是两

java并发编程实战学习笔记之对象的组合与基础构建模块

第四章 对象的组合 4.1 构建安全的类 4.2 实例封闭 @ThreadSafe public class PersonSet {     @GuardedBy("this") private final Set<Person> mySet = new HashSet<Person>();     public synchronized void addPerson(Person p) {         mySet.add(p);     }     pub

Java并发编程实践读书笔记(5) 线程池的使用

Executor与Task的耦合性 1,除非线程池很非常大,否则一个Task不要依赖同一个线程服务中的另外一个Task,因为这样容易造成死锁: 2,线程的执行是并行的,所以在设计Task的时候要考虑到线程安全问题.如果你认为只会在单任务线程的Executor中运行的话,从设计上讲这就已经耦合了. 3,长时间的任务有可能会影响到其他任务的执行效率,可以让其他线程在等待的时候限定一下等待时间.不要无限制地等待下去. 确定线程池的大小 给出如下定义: 要使CPU达到期望的使用率,线程池的大小应设置为:

java并发编程实战学习笔记之第三部分:活跃性、性能与测试

第十章 避免活跃性危险 锁顺序死锁:定义锁的顺序,可以通过某种方法决定每个锁的顺序,比如hashcode或者序列号之类的 在锁的调用顺序不是很明显的情况下,在持有锁的情况下调用其他外部方法一定要注意,可以通过开放调用,避免发生死锁的危险,即使用同步代码块保护仅仅保护那些共享变量即可,但这种降低锁粒度的方法可能会使得原本大的代码块失去原子性,解决办法为:将服务的状态改为关闭之前一直持有锁,状态改变之后,其他线程也就能够看到关闭信息从而不会再次执行关闭操作... 死锁的诊断与避免:    通过try

Java并发编程实践(读书笔记) 任务执行(未完)

任务的定义 大多数并发程序都是围绕任务进行管理的.任务就是抽象和离散的工作单元.   任务的执行策略 1.顺序的执行任务 这种策略的特点是一般只有按顺序处理到来的任务.一次只能处理一个任务,后来其它任务都要等待处理.响应性很糟糕,吞吐量低.系统资源利用率低. 2.显示的为任务创建线程 为每个任务创建对应一个线程,响应快,系统资源利用路高.缺点是资源消耗量大,如果有大量任务要执行的话,系统迟早会因为无限制创建过多的线程而造成内存耗尽.特别当创建的线程数量远远大于系统的CPU核数,由于每一个核同一时

java并发编程实战学习笔记之任务执行

第六章 任务执行 6.1 在线程中执行任务 串行->多线程->有限个多线程 6.2 executor框架 通过有界队列可以防止高负荷程序过度消耗内存 java.lang.concurrent提供了线程池作为实现executor框架的一部分 executor接口:提供生产者-消费者模式 基于executor的web服务器: public class TaskExecutionWebServer {     private static final int NTHREADS = 100;