您好,欢迎访问三七文档
当前位置:首页 > 电子/通信 > 综合/其它 > Java并发编程实践-电子书-05章
第1页共32页第5章数据冲突及诊断工具MTRAT第5章数据冲突及诊断工具MTRAT...............................................................................................15.1如何避免数据冲突...........................................................................................................25.1.1数据冲突与竞争条件............................................................................................25.1.2锁与数据冲突........................................................................................................45.1.3采用原子性操作避免数据冲突...........................................................................95.1.4采用Volatile避免数据冲突..............................................................................115.1.5ThreadLocal........................................................................................................145.2使用阻塞队列的生产者-消费者模式............................................................................155.3MTRAT介绍.......................................................................................................................195.3.1有潜在数据冲突的例子......................................................................................205.3.2MTRAT软件介绍...................................................................................................225.3.3Mtrat软件测试案例...........................................................................................255.3.4Mtrat软件的其他选项.......................................................................................275.4使用MTRAT诊断数据冲突................................................................................................28参考文献:.............................................................................................................................32第2页共32页在前面的章节中,我们已经了解了线程安全和数据冲突的概念,在本章中我们将讲解如何避免数据冲突,以及如何进行诊断。由于并行程序的不确定性造成并行程序的错误很难查找,重现和调试,IBM提供的MTRAT工具可以收集程序的运行时信息,实时分析程序中所有可能的并行程序错误(如死锁、数据冲突)。5.1如何避免数据冲突在前面的章节中我们已经了解了数据冲突。当线程之间共享数据引起了并发执行程序中的同步问题就是数据冲突。Java的数据有两种基本类型内存分配模式(不算虚拟机内部类型,详细内容参见虚拟机规范):运行时栈和堆两种。由于运行时栈是线程所私有的,它主要用来保存局部变量和中间运算结果,因此它们的数据是不可能被线程之间所共享的。内存堆是创建类对象和数组地方,它们是被虚拟机内各个线程所共享的,因此如果一个线程能获得某个堆对象的引用,那么就称这个对象是对该线程可见的。编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常这些状态都是共享的、可变的。一个对象的状态就是它的数据,存储在状态变量(statevariables)中,比如实例域或静态域。对象的状态还包括了其他附属对象的域。例如,在Web网站中,我们为统计系统的点击数设计了一个计数器。由于计数器是被多用户共享的,每个用户访问时都涉及“读-改-写”等操作,由于这些操作都不是原子的,计数器有可能出现问题。两个线程在缺乏同步的条件下,试图同时更新一个计数器时。假设计数器的初始值为19,在某些特殊的分时里,每个线程都将读它的值,并看到值是19,然后同时加1,昀后都将counter设置为20。很显然,这不是我们期望发生的事情:一次递增操作凭空取消了,一次命中计数被永久地取消了。在基于Web的服务中,如果计数器出现这种问题,可能问题不大,但已经导致严重的数据完整性问题和错误。如各在其他环境中,如银行帐号管理,那就不可原谅。在并发编程环境中,这种问题有一个专用的名称叫竞争条件。5.1.1数据冲突与竞争条件第3页共32页程序中如果存在数个竞争条件,将可能导致不正确的结果。当计算的正确性依赖于运行时中相关的时序或者多线程的交替时,会产生竞争条件。换句话说,想得到正确的答案,要依赖于“幸运”的时序。昀常见的一种竞争条件是“检查再运行(check-then-act)”,使用一个潜在的过期值作为决定下一步操作的依据。在现实生活中,我们也常常会遇到竞争条件。请看下面的从银行取钱的例子。在本例中,类Account代表一个银行账户。其中变量balance是该账户的余额。【例5-1】从银行账号取钱的例子//Account.javaclassAccount{doublebalance;publicAccount(doublemoney){balance=money;System.out.println(TotleMoney:+balance);}}下面我们定义一个线程,该线程的主要任务是从Account中取出一定数目的钱。//AccountThread.javapublicclassAccountThreadextendsThread{AccountAccount;intdelay;publicAccountThread(AccountAccount,intdelay){this.Account=Account;this.delay=delay;}publicvoidrun(){if(Account.balance=100){try{sleep(delay);Account.balance=Account.balance-100;System.out.println(withdraw100successful!);}catch(InterruptedExceptione){}}elseSystem.out.println(withdrawfailed!);}第4页共32页publicstaticvoidmain(String[]args){AccountAccount=newAccount(100);AccountThreadAccountThread1=newAccountThread(Account,1000);AccountThreadAccountThread2=newAccountThread(Account,0);AccountThread1.start();AccountThread2.start();}}程序运行结果为:TotleMoney:100.0withdraw100successful!withdraw100successful!该结果非常奇怪,因为尽管账面上只有100元,但是两个取钱线程都取得了100元钱,也就是总共得到了200元钱。出错的原因在哪里呢?图5-1给出了一种导致这种结果的线程运行过程。acountThread1AcountThread2判断判断休眠取钱休眠取钱时间图5-1一种可能的线程运行过程可以看出,由于线程1在判断满足取钱的条件后,被线程2打断,还没有来得及修改余额。因此线程2也满足取钱的条件,并完成了取钱动作。从而使共享数据balance的完整性被破坏。上例中就出现了竞争条件,它使用了一个潜在的过期值作为决定下一步操作的依据。导致了数据冲突。在现实生活中,如果出现本例中的错误,那将无法容忍的。5.1.2锁与数据冲突第5页共32页上面的问题,我们可以采用互斥锁的方式来解决(也可以采用其他方式来解决)。在并发程序设计中,对多线程共享的资源或数据成为临界资源,而把每个线(进)程中访问临界资源的那一段代码段成为临界代码段。通过为临界代码段设置信号灯,就可以保证资源的完整性,从而安全地访问共享资源。为了实现这种机制,Java语言提供以下两方面的支持:1为每个对象设置了一个“互斥锁”标记。该标记保证在每一个时刻,只能有一个线程拥有该互斥锁,其它线程如果需要获得该互斥锁,必须等待当前拥有该锁的线程将其释放。该对象成为互斥对象。2为了配合使用对象的互斥锁,Java语言提供了保留字synchronized.其基本用法如下:synchronized(互斥对象){临界代码段}当一个线程执行到该代码段时,首先检测该互斥对象的互斥锁。如果该互斥锁没有被别的线程所拥有,则该线程获得该互斥锁,并执行临界代码段,直到执行完毕并释放互斥锁;如果该互斥锁已被其它线程占用,则该线程自动进入该互斥对象的等候队列,等待其它线程释放该互斥锁。如图5-2所示,左边的图形表示,一个线程获得了对象的互斥锁,等待队列中有两个线程;右边的图形表示线程1释放互斥锁后,线程2获得互斥锁。互斥对象互斥对象Thread1Thread2Thread3Thread1Thread2Thread3图5-2互斥对象及其等待队列可以看出,任意一个对象都可以作为信号灯,从而解决上面存在的问题。我们首先定义一个互斥对象类,作为信号灯。由于该对象只作为信号量使用,所以我们并不需要为它定义其它的方法。因此该类的定义极其简单。【例5-2】使用互斥锁改写例5-1首先定义一个类,利用其对象作为互斥信号灯//AccountThread2.javaclassSemaphore{}//我们可以对上面的程序进行修改,形成新的线程。publicclassAccountThread2extendsThread{Accountaccount;第6页共32页intdelay;Semaphoresemaphore;publicAccountThread2(Accountaccount,intdelay,Semaphoresemaphore){this.account=account;this.delay=delay;this.semaphore=semaphore;}publicvoidrun(){synchronized(semaphore){if(account.balance=100){t
本文标题:Java并发编程实践-电子书-05章
链接地址:https://www.777doc.com/doc-5036539 .html