使用Contemplate的ThreadSafe发现和诊断Java并发问题

(97) 2024-04-12 17:01:01

利用多核硬件的好处已被证明是困难且危险的。 编写正确安全地使用并发性的Java软件时,需要仔细考虑以考虑在并发环境中运行的影响。 错误地考虑并发性的软件可能包含间歇性缺陷,即使最严格的测试机制也无法解决。

静态分析提供了一种在程序执行之前检测并修复并发缺陷的方法。 它通过分析程序的源代码或编译后的字节码以发现隐藏在代码中的潜在错误(早于执行)来实现此目的。

Contemplate的ThreadSafe Solo是Eclipse的商业静态分析插件,专门用于发现和帮助诊断潜伏在Java程序中的并发错误。 通过专注于并发错误,ThreadSafe可以发现其他静态分析工具(无论是商业上还是免费提供的)通常会遗漏或不旨在寻找的错误。 据我们所能确定的,以下示例中的任何缺陷都没有其他Java静态分析工具发现。

在本文中,我将通过描述示例和实际OSS代码中的一些并发性错误来介绍ThreadSafe,并展示ThreadSafe的高级静态分析和紧密的Eclipse集成如何在发现这些错误之前就可以使用它们来发现和诊断这些错误。有机会达到生产的机会。 要在自己的代码上试用ThreadSafe,可以从Contemplate网站下载免费的试用版。

我在本文中用作示例的大多数并发错误是开发人员未正确同步对共享数据的访问的实例。 同时,这种错误是Java代码中最常见的并发错误形式,并且是在代码审查或测试期间最难以检测到的错误之一。 ThreadSafe可以检测到许多不正确使用同步的情况,并且,如下文所示,它还为开发人员提供了重要的上下文信息,以帮助诊断问题。

随着时间的流逝,正确的同步变得不正确

当设计一个类的实例打算由多个线程同时调用的类时,开发人员必须仔细考虑如何正确处理对同一实例的并发访问。 即使找到了良好的设计,也不容易确保将来对代码的添加会遵循精心设计的同步协议。 ThreadSafe可以帮助指出新编写的代码违反现有同步设计的情况。

对于基本的同步任务,Java提供了几种不同的功能,包括synced关键字和更灵活的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字段的访问。 已为这些方法提供了受保护的可见性,以便可以从BankAccount的子类中访问它们。 由于对BankAccount实例的任何特定的公开暴露操作都可能涉及对这些方法的复杂调用序列,这些调用序列应作为单个原子步骤执行,因此内部API方法本身不会执行任何同步。 取而代之的是,这些方法的调用者应在存储在锁定字段中的对象上进行同步,以确保相互排斥和平衡字段更新的原子性。

只要程序保持较小,并且程序的设计可以保持在单个开发人员的头脑中,那么与并发相关的问题的风险就相对较小。 但是,在任何现实世界的项目中,原始的经过精心设计的程序都会扩展,以适应新功能,通常是该项目的新手工程师使用。

现在想象一下,在编写原始代码之后的一段时间,另一个开发人员编写了BankAccount的子类来添加一些新的可选功能。 不幸的是,新开发人员不一定了解以前的开发人员所进行的同步设计,并且没有意识到必须先对存储在对象中的对象进行同步,才能调用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类的同步策略,在调用AdjustBalance()时,applyBonus()应该已同步锁定。 不幸的是,没有执行任何同步,因此BonusBankAccount的作者引入了严重的并发缺陷。

尽管这个缺陷非常严重,但是在测试甚至生产中检测它可能非常困难。 此缺陷将表现为帐户余额不一致,因为由于缺少同步,线程对余额字段所做的更新对其他线程不可见。 该程序不会崩溃,但是会以无法追踪的方式静默地产生错误的结果。 在四核硬件上运行时,使用四个线程同时将奖金和贷项同时应用于同一帐户的实验的特定运行损失了40,000个交易中的11个。

ThreadSafe可用于识别并发缺陷,例如BonusBankAccount类中引入的示例。 在上述两个类上运行ThreadSafe的Eclipse插件会产生以下输出:

(点击图片放大)

Eclipse中的ThreadSafe视图的屏幕截图

此屏幕快照显示ThreadSafe已发现字段余额不一致同步。

要获取更多上下文信息,我们可以要求ThreadSafe向我们显示对balance字段的访问,还向我们显示为每次访问持有的锁:

(点击图片放大)

ThreadSafe的Accesss视图的屏幕截图

从此视图中,我们可以轻松地看到AdjustBalance()方法中对balance字段的访问不一致地同步。 使用Eclipse的调用层次结构视图(在这里可以通过右键单击视图中的AdjustBalance()行来访问),我们可以看到有问题的代码路径来自何处。

使用Contemplate的ThreadSafe发现和诊断Java并发问题 (https://mushiming.com/)  第1张

Eclipse的调用层次结构的屏幕快照,显示了BonusBankAccount调用的AdjustBalance

访问集合时同步不正确

上面的BankAccount类演示了一种非常简单的情况,即在访问字段时出现不正确的同步。 当然,大多数Java对象是由其他对象组成的,通常是对象集合的形式。 Java提供了各种各样的集合类,每个类都有自己的要求,即对并发访问集合是否需要同步。

集合上不一致的同步可能对程序行为特别有害。 错误地同步对字段的访问可能“仅”导致丢失更新或过时的信息,而错误地同步对尚未设计用于并发使用的集合的访问,则可能导致违反集合的内部不变式。 违反集合的内部不变性可能不会立即引起可见的影响,但可能会在程序执行的稍后一点引起奇怪的行为,包括无限循环或数据损坏。

Apache JMeter提供了一个访问共享集合时同步使用不一致的示例, Apache JMeter是一种流行的开源工具,用于在负载下测试应用程序的性能。 在Apache JMeter的2.10版上运行ThreadSafe会产生以下警告:

使用Contemplate的ThreadSafe发现和诊断Java并发问题 (https://mushiming.com/)  第2张

屏幕快照RespTimeGraphVisualizer.internalList中存储的集合上不一致的同步警告的屏幕快照:List <RespTimeGraphDataBean>

和以前一样,通过向我们显示对该字段的访问以及所持有的锁,我们可以向ThreadSafe寻求有关此报告的更多信息:

(点击图片放大)

ThreadSafe的访问权限的屏幕快照查看internalList

现在我们可以看到有三种方法可以访问存储在internalList字段中的集合。 这些方法之一是actionPerformed,它将由UI线程上的Swing Gui框架调用。

访问内部列表中存储的集合的另一种方法是add()。 再次,通过研究此方法的可能调用者,我们可以看到,确实是从不是应用程序主UI线程的线程的run()方法调用了它,这表明应该使用同步。

使用Contemplate的ThreadSafe发现和诊断Java并发问题 (https://mushiming.com/)  第3张

Eclipse的调用层次结构的屏幕快照,显示了run()方法

使用Android框架时缺少同步

应用程序可以在其中运行的并发环境几乎永远不受应用程序开发人员的完全控制。 框架响应于用户,网络或其他外部事件来调用应用程序的各个部分,并且通常具有关于可以在哪个线程上调用哪些方法的隐式要求。

在Android的K9Mail电子邮件客户端的最新Git版本中可以看到错误使用框架的示例(我们提供了到本文末尾所测试的精确版本的链接)。 在K9Mail上运行ThreadSafe会显示以下警告,表明mDraftId字段似乎是从Android后台线程和另一个线程访问的, 没有同步。

使用Contemplate的ThreadSafe发现和诊断Java并发问题 (https://mushiming.com/)  第4张

ThreadSafe关于异步回调方法中非同步访问的报告

使用ThreadSafe的访问视图,我们可以看到已经从doInBackground方法访问了mDraftId字段。

(点击图片放大)

ThreadSafe的访问视图显示有关对mDraftId的单个访问的信息

doInBackground方法是Android框架AsyncTask工具的一部分,用于在后台与主UI线程分开运行耗时的任务。 正确使用AsyncTask.doInBackground(..)可以确保Android应用程序保持对用户输入的响应,但是必须注意确保正确同步后台线程和主UI线程之间的交互。

进一步研究,使用Eclipse的调用层次结构功能,我们发现onDiscard()方法(也访问mDraftId字段)由onBackPressed()方法调用。 反过来,此方法总是由Android框架在主UI线程上调用, 而不是在运行AsyncTasks的后台线程上调用,这表明存在潜在的并发缺陷。

错误使用同步数据结构

对于相对简单的场景,Java的内置同步收集工具可以提供适当级别的线程安全,而无需花费太多精力。

同步的集合包装现有的集合,提供与基础集合相同的接口,但是同步在同步的集合实例上的所有访问。 通过使用对类似于以下内容的特殊静态方法的调用来创建同步集合:

private List<X> threadSafeList =
         Collections.synchronizedList(new LinkedList<X>());

与其他一些线程安全的数据结构相比,同步集合相对易于使用,但是它们的使用仍然涉及一些微妙的问题。 同步集合的一个常见错误是在不首先对集合本身进行同步的情况下对其进行迭代。 由于未强制访问集合,因此有可能在其他元素上进行迭代时由其他线程修改该集合。 这可能会导致ConcurrentModificationExceptions在运行时间歇性抛出,或者是不确定性,具体取决于线程的确切调度。 JDK API文档中明确记录了同步要求:

当用户遍历返回的列表时,必须手动对其进行同步:

List list = Collections.synchronizedList(new ArrayList());
   ...
synchronized (list) {
Iterator i = list.iterator(); // Must be in synchronized block while (i.hasNext())
foo(i.next());
}

不遵循该建议可能导致不确定的行为。

但是,在同步已同步的集合上进行迭代时,很容易忘记进行同步,尤其是因为它们具有与普通未同步的集合完全相同的接口。

可以在Apache JMeter的 2.10版中找到这种错误的示例。 ThreadSafe报告以下实例“同步集合上的不安全迭代”:

使用Contemplate的ThreadSafe发现和诊断Java并发问题 (https://mushiming.com/)  第5张

ThreadSafe的不安全迭代报告的屏幕截图

ThreadSafe报告的行包含以下代码:

Iterator<Map.Entry<String, JMeterProperty>> iter = propMap.entrySet().iterator();

在这里,迭代遍历了通过调用entrySet()创建的同步集合上的视图 。 由于对集合的看法是“实时的”,因此如上所述,此代码具有确定性行为和ConcurrentModificationExceptions的相同潜力。

结论

我介绍了一些在现实世界的Java程序中常见的并发缺陷,并展示了如何使用Contemplate的ThreadSafe发现和诊断它们。

通常,静态分析工具提供了一种有希望的方法来减轻现有Java代码和新Java代码中潜在的缺陷风险。 静态分析通过提供一种快速且可重复的方式来扫描著名但细微而严重的错误的代码,从而补充了诸如测试和代码审查之类的传统软件质量技术。 特别是并发错误,由于它们依赖于并发线程的不确定性调度,因此很难通过测试可靠地找到。

ThreadSafe能够发现一系列其他并发缺陷,包括由于并发集合框架使用不当而导致的原子性错误,以及由于滥用阻塞方法而可能导致的死锁。 ThreadSafe技术简介和演示视频提供了ThreadSafe可以检测到的细微但潜在的灾难性缺陷的更多示例。

资源资源

  • Contemplate网站 -包括有关ThreadSafe的更多信息,有关如何请求试用版以及如何购买的信息。
  • Apache JMeter 2.10的下载-可以下载用于以上演示的源代码。
  • k9mail Git存储库 -可以下载K9Mail的源代码。 本文使用的版本具有提交的SHA1 ID: b500047e426baa0807570c2f2836d0cf9ba6cc19

翻译自: https://www.infoq.com/articles/Java-Concurrency-Static-Analysis-with-ThreadSafe/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

THE END

发表回复