13

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

public class MyValueHolder {

    private int value = 1;

    // ... getter and setter

}

当线程 A 这样做myValueHolder.setValue(7)时,不能保证线程 B 会读取该值:myValueHolder.getValue()理论上可以1永远返回。

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

有没有办法让 JVM 模拟最坏的情况,它会1永远返回线程 B?在这种情况下,这对于使用我们现有的测试来测试我们的多线程代码非常有用。

4

7 回答 7

28

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

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

如果有一个 Java 编译器可以有意地执行尽可能多的奇怪(但允许)转换,以便能够更容易地破坏线程不安全代码,就像C 的Csmith一样。不幸的是,这样的编译器不存在(到目前为止我所知)。

同时,您可以尝试jcstress库* 并在多个架构上练习您的代码,如果可能的话,使用较弱的内存模型(即不是 x86)来尝试破坏您的代码:

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

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

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

于 2013-07-22T10:38:39.737 回答
2

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

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

迫使这种情况在真实机器上发生的唯一方法是更改​​代码以使您的测试策略无效,即您最终测试不同的代码。

但是,您可能能够在模拟硬件的模拟器上运行它,该硬件不会清除二级缓存。虽然听起来很努力!

于 2013-07-11T09:39:25.600 回答
2

我认为您指的是称为“错误共享”的原则,其中不同的 CPU 必须同步其缓存,否则将面临您描述的数据可能不匹配的可能性。 英特尔网站上有一篇关于虚假分享的非常好的文章。 英特尔在他们的文章中描述了一些有用的工具来诊断这个问题。这是一个相关的报价:

避免错误共享的主要方法是通过代码检查。线程访问全局或动态分配的共享数据结构的实例是错误共享的潜在来源。请注意,线程可能正在访问恰好在内存中相对靠近的完全不同的全局变量这一事实可能会掩盖错误共享。可以排除线程局部存储或局部变量作为错误共享的来源。

尽管文章中描述的方法不是您所要求的(从 JVM 强制执行最坏情况的行为),但正如已经说过的那样,这实际上是不可能的。本文中描述的方法是我所知道的尝试诊断和避免虚假共享的最佳方法。

网上还有其他资源可以解决这个问题。 例如,这篇文章对避免在 Java 中进行错误共享的方法提出了建议。 这种方法我没试过,所以不能保证,但我觉得作者的想法是合理的。你可以考虑试试他的建议。

于 2013-07-22T00:14:07.443 回答
2

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

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

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

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

跨JVM的线程间通信

于 2014-08-18T15:43:30.307 回答
1

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

为了找到这种不正确同步的代码块,我使用以下算法:

使用仪器记录所有现场修改的线程。如果一个字段在没有同步的情况下被多个线程修改,我发现了数据竞争。

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

于 2013-10-26T14:03:16.123 回答
-2

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

于 2013-07-21T05:12:08.980 回答