如何使用Contemplate ThreadSafe发现并判断Java并发问题2014-08-30 infoq 译:张卫滨事实证明,要发挥多核硬件所带来的收益是很困难和有风险的。当使用并发正确和安全地编写Java软件时,我们需要很仔细地进行思考。因为错误使用并发会导致偶尔才出现的缺陷,这些缺陷甚至能够躲过最严格的测试环境。静态分析工具提供了一种方式,可以在代码执行之前探查并修正并发错误。它能够在代码执行之前分析程序的源码或编译形成的字节码,进而发现隐藏在代码之中的缺陷。Contemplate的ThreadSafe Solo是一个商用的Eclipse静态分析插件,其目的就是专门用来发现并诊断隐藏在Java程序之中的缺陷。因为专注于并发方面的缺陷,所以ThreadSafe能够发现其他商用或免费静态分析工具无法发现的缺陷,这些工具通常会忽视这种缺陷或者根本就不是为了查找这种缺陷而设计的。就目前我们所能确定的,其他的Java静态分析工具都不能捕获以下样例中的任何缺陷。在本文中,我会通过一系列并发缺陷来介绍ThreadSafe,这些都是具体的样例和实际的OSS代码,这里展现了ThreadSafe的高级静态分析以及与Eclipse的紧密集成,这样我们就能在代码产品化之前,及早发现并诊断这些缺陷。如果想在你的代码上体验ThreadSafe的话,可以在上下载免费试用版本。在本文中作为样例所使用的并发缺陷都是由于开发人员没有正确地同步对共享数据的访问所引起的。这类缺陷同时是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的作者在这里引入了一个严重的并发缺陷。尽管这个缺陷很严重,但是在测试甚至生产阶段要探测到它却是很困难的。这个缺陷的表现形式为不一致的账户余额,这是由于缺少同步会导致某个线程对balance域的更新对其他线程是不可见的。这个缺陷不会导致程序崩溃,但是会以难以跟踪的方式,默默地产生不一致的结果。在四核的硬件上,尝试以四个线程并发地对同一个账户进行返现和贷出操作,在40,000个事务中会有11个是失效的。ThreadSafe可以用来识别类似于BonusBankAccount类所引入的并发缺陷。在上面提到的两个类上运行ThreadSafe的Eclipse插件,会产生如下的输出: