Java中的多线程状态可见性:有没有办法将JVM变成最坏的情况?

时间:2013-07-11 09:26:15

标签: java concurrency jvm race-condition jvm-arguments

假设我们的代码有2个线程(A和B)在某个地方引用了这个类的同一个实例:

public class MyValueHolder {

    private int value = 1;

    // ... getter and setter

}

当线程A执行myValueHolder.setValue(7)时,无法保证线程B会读取该值:myValueHolder.getValue()理论上可以永久地返回1

但实际上,硬件迟早会清除二级缓存,因此线程B迟早会读取7(通常会更早)。

有没有办法让JVM模拟最糟糕的情况,它会一直为线程B返回1这对测试我们的多线程代码非常有用在这种情况下我们现有的测试。

7 个答案:

答案 0 :(得分:27)

这里有

jcstress维护者。有多种方法可以回答这个问题。

  1. 最简单的解决方案是将getter包装在循环中,让JIT将其提升。这适用于非易失性字段读取,并通过编译器优化模拟可见性故障。
  2. 更复杂的技巧涉及获取OpenJDK的调试版本,并使用-XX:+StressLCM -XX:+StressGCM,有效地执行指令调度模糊测试。有问题的负载可以浮动到您可以通过产品的常规测试检测到的地方。
  3. 我不确定是否有实用的硬件将写入的值保持足够长的不透明缓存一致性,但使用jcstress构建测试用例有点容易。你必须记住,(1)中的优化也可能发生,所以我们需要采用一种技巧来防止这种情况。我认为like this应该有用。

答案 1 :(得分:3)

如果有一个Java编译器可以有意地执行尽可能多的奇怪(但允许)的transfirmations以便更容易地破坏线程不安全的代码,比如Csmith用于C,那将是很棒的。不幸的是,这样的编译器不存在(据我所知)。

与此同时,您可以尝试jcstress库*并在多个体系结构上运行您的代码,如果可能的话,使用weaker memory models(即不是x86)来尝试破坏您的代码:

  

Java并发压力测试(jcstress)是一个实验性工具,一套测试有助于研究JVM,类库和硬件中并发支持的正确性。

但最后,不幸的是,证明一段代码100%正确的唯一方法是代码检查(我不知道能够检测所有竞争条件的静态代码分析工具)。

*我没有使用它,我不清楚jcstress和the java-concurrency-torture library哪个更新(我怀疑是jcstress)。

答案 2 :(得分:2)

不在真机上,遗憾的是测试多线程代码仍然很困难。

正如您所说,硬件将清除二级缓存,而JVM无法控制它。 JSL仅指定必须发生的情况,这是B 可能永远不会看到value的更新值的情况。

强制这种情况发生在真机上的唯一方法是改变代码,以便使测试策略无效,即最终测试不同的代码。

但是,您可以在模拟器上运行此模拟器,该模拟器模拟不清除二级缓存的硬件。虽然听起来很费劲!

答案 3 :(得分:2)

我认为您正在引用称为“错误共享”的原则,其中不同的CPU必须同步其缓存,否则将面临您描述的数据可能变得不匹配的可能性。 There is a very good article on false sharing on Intel's website.英特尔在其文章中介绍了一些用于诊断此问题的有用工具。这是一个相关的引用:

  

避免错误共享的主要方法是通过代码   检查。线程访问全局或动态的实例   分配的共享数据结构是潜在的错误来源   分享。请注意,虚假共享可能会被以下事实所掩盖   线程可能正在访问完全不同的全局变量   碰巧在记忆中相对接近。线程本地存储   或局部变量可以排除为虚假共享的来源。

虽然本文中描述的方法不是您要求的(强制JVM的最坏情况行为),但如前所述,这实际上是不可能的。本文中描述的方法是我所知道的尝试诊断和避免错误共享的最佳方法。

还有其他资源可以解决网络上的这个问题。 For example, this article has a suggestion for a way to avoid false sharing in Java.我没有尝试过这种方法,所以我无法保证,但我认为作者的想法是合理的。您可以考虑尝试他的建议。

答案 4 :(得分:2)

我之前曾建议在内存模型列表中出于测试目的使用JVM的最坏情况,但这个想法似乎并不受欢迎。

那么如何利用现有技术获得“最坏情况的JVM行为”,即如何在问题中测试场景并使其每次都失败。您可以尝试使用最弱的内存模型找到设置,但这不太可能是完美的。

我经常考虑的是使用分布式JVM类似于我认为Terracotta在封面下工作的方式,因此您的应用程序现在可以在多个JVM(远程或本地)上运行(同一应用程序中的线程在不同的实例中运行)。在此设置中,JVM间线程通信发生在内存屏障处,例如例如,错误代码中缺少的同步关键字(它符合Java内存模型)并且应用程序已配置,即您说此类线程在此处运行。您的测试只需配置更改代码,任何有序的Java应用程序都应该是开箱即用的,但是这种设置对于错误排序的应用程序是非常不容忍的(通常是一个问题......现在是一个资产,即内存模型展示非常弱但合法的行为)。在上面的示例中,将代码加载到集群上,如果两个线程在不同的节点上运行,则 setValue 对其他线程没有任何影响,除非代码被更改并同步,使用了volatile等,然后代码按预期工作。

现在,您对上述示例(正确配置)的测试每次都会失败而没有正确的“在订购之前发生”,这对测试可能非常有用。完全覆盖计划中的缺陷您需要每个应用程序线程(可以是同一台机器或群集中的多个)或多个测试运行的潜在节点。如果你有1000个线程,那么这可能是令人望而却步的,但希望它们可以汇总并按比例缩小以用于E2E测试场景或在云中运行。如果没有别的,这种设置可能有助于证明这个问题。

inter thread communication across JVMs

答案 5 :(得分:1)

您提供的示例在http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4中被描述为错误同步。我认为这总是不正确的,迟早会导致错误。大多数时候以后: - )。

要找到这种错误同步的代码块,我使用以下算法:

使用检测记录所有字段修改的线程。如果一个字段被多个线程修改而没有同步,我发现了一个数据竞争。

我在http://vmlens.com中实现了这个算法,这是一个在java程序中查找数据竞争的工具。

答案 6 :(得分:-2)

这是一个简单的方法:只需注释掉setValue的代码。您可以在测试后取消注释。由于在许多情况下需要一种机制来伪造故障,因此为所有这些情况构建一般机制是个好主意。