如何利用Contemplate ThreadSafe发明并判定Java并发问题
副标题#e#
事实证明,要发挥多核硬件所带来的收益是很坚苦和有风险的。当利用并发正确和安详地编写Java软件时,我们需要很仔细地举办思考。因为错误利用并发会导致偶然才呈现的缺陷,这些缺陷甚至可以或许躲过最严格的测试情况。
静态阐明东西提供了一种方法,可以在代码执行之前探查并批改并发错误。它可以或许在代码执行之前阐明措施的源码或编译形成的字节码,进而发明埋没在代码之中的缺陷。
Contemplate的ThreadSafe Solo是一个商用的Eclipse静态阐明插件,其目标就是专门用来发明并诊断埋没在Java措施之中的缺陷。因为专注于并发方面的缺陷,所以ThreadSafe可以或许发明其他商用或免费静态阐明东西无法发明的缺陷,这些东西凡是会忽视这种缺陷可能基础就不是为了查找这种缺陷而设计的。就今朝我们所能确定的,其他的Java静态阐明东西都不能捕捉以下样例中的任何缺陷。
在本文中,我会通过一系列并发缺陷来先容ThreadSafe,这些都是详细的样例和实际的OSS代码,这里揭示了ThreadSafe的高级静态阐明以及与Eclipse的细麋集成,这样我们就能在代码产物化之前,赶早发明并诊断这些缺陷。假如想在你的代码上体验ThreadSafe的话,可以在Contemplate站点上下载免费试用版本。
在本文中作为样例所利用的并发缺陷都是由于开拓人员没有正确地同步对共享数据的会见所引起的。这类缺陷同时是Java代码中最常见的并发缺陷形式,也是在代码查抄和测试中最难探查的缺陷之一。ThreadSafe可以或许探测出浩瀚没有正确利用同步的场景,如下文所述,它同时还能为开拓人员提供至关重要的上下文信息,从而有助于对问题做出诊断。
原本正确的同步跟着时间的推移变得不正确了
假如一个类的实例会被多个线程并发挪用,那么在设计的时候,开拓人员必需要仔细思量如何对同一个实例举办并发会见,以担保可以或许正确地举办处理惩罚。即便找到了好的设计方案,也很难担保这个颠末仔细设计的同步协议在未来添加代码时可以或许获得充实的尊重。当新编写的代码违反已有的并发设计时,ThreadSafe可以或许辅佐指出这些场景。
对付简朴的同步任务,Java提供了多种差异的基本设施,包罗synchronized要害字以及更为机动的java.util.concurrent.locks包。
作为一个简朴示例,我们利用Java内置的同步设施来安详并发地会见共享资源,思量如下的代码片断,实现了模仿的“银行账户”类。
public class BankAccount { protected final Object lock = new Object(); private int balance; protected int readBalance() { return balance; } protected void adjustBalance(int adjustment) { balance = balance + adjustment; } // ... methods that synchronize on "lock" while calling // readBalance() or adjustBalance(..) }
这个类的开拓人员抉择通过两个内部的API要领,即readBalance()和adjustBalance(),来对balance域提供会见成果。这些要领给定了protected级此外可见性,所以它们大概会被BankAccount的子类会见。鉴于在BankAccount实例上任何对外袒露的特定操纵城市涉及到对这些要领举办一系列巨大的挪用,这些要领应该作为一个原子的步调来执行,而内部的API要领自己并不举办任何的同步。相反,这些要领的挪用者要同步lock域中所存储的工具,以担保互斥性以及对balance域更新的原子性。
在措施局限很小的时候,措施的设计可以装在某个开拓人员的脑筋中,呈现并发相关问题的风险相对来讲会较量小。可是,在实际的项目中,最初经心设计的措施需要举办扩展以适应新的成果,而这凡是是由项目标新工程师来完成的。
此刻,假设在最初的代码编写一段时间之后,别的一个开拓人员编写了BankAccount的子类来添加一些新的可选成果。令人遗憾的是,这个新的开拓人员并不必然相识之前的开拓人员所设计好的同步机制,他并没有意识到假如没有预先同步生存在lock域中的工具,是不能挪用readBalance()和adjustBalance(..)的。
新工程师所编写的BankAccount子类代码大概会如下所示:
public class BonusBankAccount extends BankAccount { private final int bonus; public BonusBankAccount(int initialBalance, int bonus) { super(initialBalance); if (bonus < 0) throw new IllegalArgumentException("bonus must be >= 0"); this.bonus = bonus; } public void applyBonus() { adjustBalance(bonus); } }
在applyBonus()的实现中存在着问题。为了正确地遵循BankAccount类的同步计策,applyBonus()在挪用adjustBalance()时应该同步lock。不外,这里没有执行同步,所以BonusBankAccount的作者在这里引入了一个严重的并发缺陷。
#p#分页标题#e#
尽量这个缺陷很严重,可是在测试甚至出产阶段要探测到它却是很坚苦的。这个缺陷的表示形式为纷歧致的账户余额,这是由于缺少同步会导致某个线程对balance域的更新对其他线程是不行见的。这个缺陷不会导致措施瓦解,可是会以难以跟踪的方法,冷静地发生纷歧致的功效。在四核的硬件上,实验以四个线程并发地对同一个账户举办返现和贷出操纵,在40,000个事务中会有11个是失效的。
ThreadSafe可以用来识别雷同于BonusBankAccount类所引入的并发缺陷。在上面提到的两个类上运行ThreadSafe的Eclipse插件,会发生如下的输出:
查察本栏目
#p#副标题#e#
在Eclipse中,ThreadSafe视图的截屏
这个截屏显示ThreadSafe已经发明balance域没有举办一致的同步。
要获取更多的上下文信息,可以让ThreadSafe显示对balance域的会见,它还会为我们揭示每次会见所持有的锁:
ThreadSafe Accesses视图的截屏
通过这个视图,我们可以清楚地看到在adjustBalance()要领中对balance域没有举办一致性的同步。利用Eclipse的挪用层级(call hierarchy)视图(在这里可以通过右键点击视图中adjustBalance()这一行快速会见),我们可以看到这个讨厌的代码路径是奈何发生的。
Eclipse挪用层级的截屏,揭示了BonusBankAccount对adjustBalance要领的挪用
会见集适时,不正确的同步
上面提到的BankAccount类是一个很简朴的例子,揭示了会见域时没有举办正确的同步。虽然,大大都Java工具都是由其他工具构成的,常见的表示形式就是工具荟萃。Java提供了种类繁多的荟萃类,当对荟萃举办并发会见时,每一个荟萃类都有其是否需要举办同步的需求。
对荟萃的纷歧致同步大概会对措施的行为带来出格严重的影响。当对一个域的会见没有正确的同步时,大概“只是”丢失更新或利用逾期数据,而有些荟萃原本并没有设计成支持并发利用,对这些荟萃的纷歧致同步则大概会违反荟萃内部的稳定形(invariants)。假如违反了荟萃的内部稳定形大概并不会顿时呈现可见性的问题,可是大概会导致很诡异的行为,好比在措施的后续执行中会呈现无限轮回或数据损坏。
当会见共享的集适时,纷歧致地利用同步的样例呈此刻Apache JMeter之中,这是一个很风行的测试应用在负载下机能的开源东西。在2.1.0版本的Apache JMeter上运行ThreadSafe会发生如下的告诫:
存储在RespTimeGraphVisualizer.internalList : List<RespTimeGraphDataBean>域中的荟萃因为纷歧致同步而发生的告诫截屏
像前面一样,我们可以要求ThreadSafe揭示这个陈诉的更多信息,包罗对这个域的会见以及它所持有的锁:
探查internalList的ThreadSafe Accesses视图的截屏
此刻我们可以看到有三个要了解见存储在internalList域中的荟萃。个中有一个要领是actionPerformed,它将会由Swing Gui框架在UI线程上挪用。
别的一个会见internalList所存储荟萃的要领是add()。同样的,探查这个要领大概的挪用者,我们会发明它确实会由一个线程的run()来挪用,而这个线程并不是应用的UI线程,这表白应该要利用同步。
Eclipse的挪用层级布局截屏,揭示了run()要领
查察本栏目
当利用Android框架时,缺少同步
应用措施运行时地址的并发情况凡是并不在应用开拓人员的节制之下。框架会挪用各个部门来响应用户、网络或其他的外部事件,凡是来讲某个要领能被哪条线程来挪用都有内涵的需求。
未正确利用框架的一个样例可以在Git版本的Android email客户端K9Mail上找到(在本文的末了处,我们提供了所测试版本的链接)。在K9Mail上运行ThreadSafe会获得如下的告诫,表白mDraftId域会被Android的靠山历程以及别的一个历程所会见,可是没有举办同步。
针对异步回调要领的未同步会见,ThreadSafe所发生的陈诉
利用ThreadSafe的Accesses视图,我们可以看到mDraftId域会被名为doInBackground的要领所会见。
ThreadSafe的Accesses视图揭示了对mDraftId的每个会见
#p#分页标题#e#
doInBackground要领是Android框架AsyncTask基本设施的一部门,它用来在靠山执行耗时的任务,这是与主UI线程相疏散的。正确利用AsyncTask.doInBackground(..)可以或许担保对用户的输入保持响应,可是必需要留意的是靠山线程与主UI线程之间的交互必需要正确地同步。
进一步举办探查,利用Eclipse的挪用层级布局视图,我们会发明onDiscard()要领,这个要领也会见了mDraftId域,这个要领是被onBackPressed()所挪用的。而这个要领凡是是由Android框架在主线程中挪用的,并不是运行AsyncTasks的靠山线程,这就表白这里会有一个潜在的并发缺陷。
不正确地利用同步的数据布局
对付相对简朴场景,Java内置的同步荟萃就提供了符合的线程安详性成果,无需我们费太多工夫。
同步荟萃对原有的荟萃类举办了包装,提供了与底层荟萃沟通的接口,可是对同步荟萃实例的所有会见都举办了同步。同步荟萃要通过挪用特定的静态要领来得到,雷同的挪用方法如下所示:
private List<X> threadSafeList = Collections.synchronizedList(new LinkedList<X>());
相对付其他线程安详的数据布局,同步荟萃利用起来很容易,可是在它们的利用中也有很微妙的陷阱。在利用同步集适时,一个常见的错误就是在没有同步荟萃自己的环境下,对它们举办遍历。鉴于没有强制要求对荟萃举办排他性的会见,所以在迭代其元素的时候,荟萃大概会被其他的线程修改。这大概会导致间歇性地抛出ConcurrentModificationException,可能呈现无法预知的行为,这取决于线程的详细调治。同步的需求明晰记录在JDK API文档之中:
查察本栏目
尽量如此,当迭代一个同步集适时,照旧很容易健忘举办同步的,尤其是它们与通例的非同步荟萃有着沟通的接口。
在2.10版本的Apache JMeter之中,可以看到这种错误的样例。ThreadSafe陈诉了如下“对同步荟萃的不安详遍历”场景:
ThreadSafe所发生的陈诉不安详遍历的截屏
ThreadSafe陈诉的那一行中包括了如下的代码:
Iterator<Map.Entry<String, JMeterProperty>> iter = propMap.entrySet().iterator();
在这里,迭代是基于一个同步荟萃的视图(view)举办的,它是通过挪用entrySet()获得的。因为荟萃的视图是“活泼的(live)”,因此这段代码同样大概发生上文所述的无法预知行为或ConcurrentModificationException。
结论
我揭示了一小部门并发相关的缺陷,这些都是在实际的Java措施中很常见的,而且演示了Contemplate ThreadSafe可以或许如何辅佐我们发明并诊断它们。
总体而言,不管是已有的照旧新编写的Java代码,静态阐明东西都能有助于发明埋没在代码之中的缺陷。静态阐明可以或许对传统的软件质量技能形成增补,这些传统的技能包罗测试和代码审查,静态阐明提供了一种快捷且可反复的方法来扫描代码,目标在于发明一些为各人所熟知可是较量难以发明且严重的缺陷。并发的缺陷尤其难以在测试中很靠得住的发明,因为它们依赖于不确定的并发线程调治。
ThreadSafe还能发明其他一系列的并发缺陷,包罗因为不正确地利用并发荟萃框架所引起的原子性错误以及错误利用阻塞要领大概引起的死锁。ThreadSafe的技能资料以及样例视频中展示了ThreadSafe可以或许发明的更多缺陷样例,这些缺陷难以被发明,但很大概是劫难性的。
当对返回的list举办遍历的时候,用户必需手动地对其举办同步:
List list = Collections.synchronizedList(new ArrayList()); ... synchronized (list) { Iterator i = list.iterator(); // Must be in synchronized block while (i.hasNext()) foo(i.next()); }
不遵循该发起的话大概会导致无法预知的行为