【问题标题】:How to test visibility of values between threads如何测试线程之间值的可见性
【发布时间】:2009-09-12 06:07:06
【问题描述】:

测试线程之间值可见性的最佳方法是什么?

class X {
 private volatile Object ref;

 public Object getRef() {
  return ref;
 }

 public void setRef(Object newRef) {
  this.ref = newRef;
 }
}

X 类公开了对ref 对象的引用。如果并发线程读取和写入对象引用,则每个线程都必须查看设置的最新对象。 volatile 修饰符应该这样做。这里的实现是一个例子,它也可以是同步的或基于锁的实现。

现在我正在寻找一种方法来编写一个测试,当值的可见性未按规定(读取旧值)时通知我。

如果测试确实消耗了一些 cpu 周期也没关系。

【问题讨论】:

  • 也许自省允许看到 volatile 限定符
  • 目的是从外部观察线程可见性是否正确。这段代码可以使用同步,从这个角度来看是可以的。

标签: java multithreading unit-testing testing concurrency


【解决方案1】:

JLS 说明了您应该做什么才能在涉及“线程间操作”的应用程序中获得保证的一致执行。如果你不做这些事情,执行可能会不一致。但它是否真的会不一致取决于您使用的 JVM、您使用的硬件、应用程序、输入数据以及......当您运行应用程序时机器上可能发生的任何其他事情。

我看不出你提议的测试能告诉你什么。如果测试显示不一致的执行,它将确认正确进行同步的智慧。但是,如果运行几次测试只显示(显然)一致的执行,这并不能告诉您执行总是一致的。


例子:

假设您在(例如)JDK 1.4.2(rev 12)/Linux/32 位上运行测试,其中“客户端”JVM 和选项 x、y、z 在单处理器机器上运行。在运行测试 1000 次后,您会发现 如果您忽略 volatile,它似乎没有任何区别。在这种情况下,您实际上学到了什么?

  • 您还没有了解到它真的没有区别吗?如果您更改测试以使用更多线程等,您可能会得到不同的答案。如果您再运行数千次、数百万次或数十亿次测试,您可能会得到不同的答案。
  • 您尚未了解其他平台上可能发生的情况;例如不同的 Java 版本、不同的硬件或不同的系统负载条件。
  • 您还没有了解省略volatile 关键字是否安全

只有在测试显示出差异时,您才能学到一些东西。你唯一学到的是同步很重要 ...这是所有教科书等一直在告诉你的:-)


底线:这是最糟糕的黑盒测试。它无法让您真正了解盒子内部发生的事情。要获得这种洞察力,您需要 1) 了解内存模型和 2) 深入分析 JIT 编译器发出的本机代码(在多个平台上......)

【讨论】:

  • 如果您为方法“String x(String y)”编写测试,您确实知道没有机会测试所有字符串值。尽管如此,您尝试测试每个等价类的代表值。这个问题比较模糊。这取决于 vm 的实现,甚至可能取决于 vm 的配置。
  • 编辑:理论上你可以控制虚拟机。它可能是一个在解释 java 内存模型时非常激进的实现。这并不容易,而且对于一次测试运行来说也不是确定性的,但比“这方面不可测试”更可行且更好。
  • “你还没有学过……”:在现实中测试代码不会那么容易。它可以以多种方式实现(易失性、锁定、同步、行为不端的实现)。它可能是一个集成测试,其中许多单元的行为会导致可见性问题。 (这更像是一个设计问题,但真正的代码就是这样。)你必须找出你的代码有问题。曾经在没有 jdepend 的代码库中进行过循环依赖分析吗?祝你好运!相比之下,这个问题微不足道,而且很多(大多数?)代码都不正确。解决这个问题并不是说你能学到很多东西。
  • @Thomas:我的意思是,编写测试来测试(故意)错误同步的效果是没有意义的。你知道这是错误的......所以不要这样做。
  • @Thomas:与此相反的是,实际上不可能测试您是否正确完成了同步。
【解决方案2】:

如果我理解正确,那么您需要一个测试用例,如果变量定义为 volatile,则通过,否则失败。

但是我认为没有可靠的方法可以做到这一点。取决于 jvm 的实现,即使没有 volatile,并发访问也可能正常工作。

因此,当指定 volatile 时,单元测试正常工作,但在没有 volatile 时它仍然可能正常工作。

【讨论】:

  • 确实,这样的测试是不可靠的
  • 这类问题的本质是不确定性,取决于虚拟机实现的特性。要知道它在一个设置中工作对你的代码来说是非常重要的。如果无法观察到,您总是会迷失在测试中。目标是拥有这样的测试覆盖率,如果您删除了代码的某个方面,您的测试就会报错。
【解决方案3】:

哇,这比我最初想象的要困难得多。 我可能完全不在了,但是这个怎么样?

class Wrapper {
    private X x = new X();
    private volatile Object volatileRef;
    private final Object setterLock = new Object();
    private final Object getterLock = new Object();

    public Object getRef() {
        synchronized(getterLock) {
            Object refFromX = x.getRef();
            if (refFromX != volatileRef) {
                // FAILURE CASE!
            }
            return refFromX;
        }
    }

    public void setRef(Object ref) {
        synchronized(setterLock) {
            volatileRef = ref;
            x.setRef(ref);
        }
    }
}

这有帮助吗? 当然,您将不得不创建许多线程来命中这个包装器,希望出现坏情况。

【讨论】:

  • 这个问题是 synchronized(setterLock) { volatileRef = ref; x.setRef(ref); } 对读者来说不是原子的。
【解决方案4】:

这个怎么样?

public class XTest {

 @Test
 public void testRefIsVolatile() {
  Field field = null;
  try {
   field = X.class.getDeclaredField("ref");
  } catch (SecurityException e) {
   e.printStackTrace();
   Assert.fail(e.getMessage());
  } catch (NoSuchFieldException e) {
   e.printStackTrace();
   Assert.fail(e.getMessage());
  }
  Assert.assertNotNull("Ref field", field);
  Assert.assertTrue("Is Volatile", Modifier.isVolatile(field
    .getModifiers()));
 }

}

【讨论】:

  • 测试必须对代码一无所知。只要可见性正确,每个实现(同步、锁定、易失)都可以。
【解决方案5】:

所以基本上你想要这样的场景:一个线程写入变量,而另一个线程同时读取它,并且你想确保读取的变量具有正确的值,对吧?

好吧,我认为您不能为此使用单元测试,因为您无法确保正确的环境。这是由 JVM 通过调度指令的方式来完成的。这就是我要做的。使用调试器。启动一个线程来写入数据并在执行此操作的行上放置一个断点。启动第二个线程并让它读取数据,此时也停止。现在,让第一个线程执行写入的代码,然后用第二个线程读取。在您的示例中,您将无法实现任何目标,因为读取和写入是单个指令。但通常如果这些操作比较复杂,可以交替执行两个线程,看看是否一切一致。

这需要一些时间,因为它不是自动化的。但是我不会去写一个尝试多次读写的单元测试,希望能抓住失败的情况,因为你什么也做不了。单元测试的作用是确保您编写的代码按预期工作。但是在这种情况下,如果测试通过了,你就不能放心了。也许只是幸运,这次冲突并没有出现。这违背了目的。

【讨论】:

    猜你喜欢
    • 2013-04-22
    • 2016-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多