代码之家  ›  专栏  ›  技术社区  ›  Andrzej Doyle

在Java中测试线程安全性的方法是否令人满意?

  •  40
  • Andrzej Doyle  · 技术社区  · 17 年前

    我希望改进一个包,当它的输入在多个工作线程之间共享时,我认为它不是线程安全的。根据TDD原则,我应该首先编写一些失败的测试,这些测试在评估问题时肯定是有用的。

    我认识到这不是一件简单的事情,简单地说,多线程测试将是不确定的,因为操作系统将决定调度和各种操作交错的确切顺序。我已经看过并用过了 MultithreadedTC 在过去,这是有用的。但是,在这种情况下,我提前知道了现有实现的具体崩溃位置,因此能够构建一组很好的测试来覆盖它。

    然而,如果你不知道问题的确切位置,那么有没有一种很好的方法来编写一个能够很好地抛出任何潜在问题的测试呢?有没有其他人认为有用的图书馆?从纯粹主义的观点来看,多线程测试用例应该和通常的单线程测试一样,只是在多个工作线程的情况下运行,我这样想是对的吗?

    欢迎提供有关工具/最佳实践/总体理念的任何报价。

    5 回复  |  直到 10 年前
        1
  •  15
  •   Rene    17 年前

    忘记通过测试并发性问题获得好的结果。尝试减少同步并使问题更小。然后使用尽可能高的库支持来进行同步。只有当你真的尝试自己处理并发时。当您知道每个工作人员的工作都是正确的,并且您的所有想法都告诉您,您已经解决了并发问题,然后生成了一些有趣的负载。UnitTest框架及其扩展可以完成这项工作,但要知道您不再测试任何单元了。(记住,您已经覆盖了该部分)

    如果您的并发模型变得复杂,请查看适合这种情况的工具 SPIN .

        2
  •  19
  •   Craig P. Motlin    17 年前

    Java Concurrency in Practice 有一些关于如何为并发性问题编写测试的重要信息。但是它们不是真正的单元测试。几乎不可能为并发问题编写真正的单元测试。

    基本上可以归结为这个。创建一组测试线程并启动它们。每个线程都应该

    • 等待倒计时闩锁
    • 重复调用修改问题中可变状态的方法
    • 倒数第二个闩锁并退出

    JUnit线程创建所有线程并启动它们,然后倒数第一个闩锁一次,让它们全部离开,然后等待第二个闩锁,然后对可变状态做出一些断言。

    甚至比其他类型的bug更容易为并发bug编写失败的单元测试。 之后 你找到了那个虫子。

        3
  •  9
  •   MiguelMunoz    13 年前

    你必须解决两个基本问题。第一个问题是:如何生成线程冲突?第二,如何验证您的测试?

    第一个是直截了当的。用大锤子。编写一些能够检测一个线程跨接另一个线程的情况的代码,并在50个连续线程上运行该代码50次左右。您可以使用倒计时闩锁执行此操作:

    public void testForThreadClash() {
      final CountDownLatch latch = new CountDownLatch(1);
      for (int i=0; i<50; ++i) {
        Runnable runner = new Runnable() {
          public void run() {
            try {
              latch.await();
              testMethod();
            } catch (InterruptedException ie) { }
          }
        }
        new Thread(runner, "TestThread"+i).start();
      }
      // all threads are waiting on the latch.
      latch.countDown(); // release the latch
      // all threads are now running concurrently.
    }
    

    testmethod()必须生成这样一种情况:如果没有同步块,某些线程将单步执行另一个线程的数据。它应该能够检测到这种情况并抛出异常。(上面的代码没有这样做。异常很可能是在某个测试线程上生成的,您需要放入一个机制,在主线程上检测它,但这是一个单独的问题。为了简单起见,我省略了它,但您可以使用第二个闩锁来完成此操作,测试可以在异常情况下提前清除该闩锁。)

    第二个问题比较棘手,但有一个简单的解决方案。问题是:你怎么知道你的锤子会产生碰撞?当然,您的代码具有同步块以防止冲突,因此您无法回答问题。但你可以。 只需删除同步关键字。 因为它是使类线程安全的同步关键字,所以您只需删除它们并重新运行测试。如果测试有效,它现在将失败。

    当我第一次写上面的代码时,结果发现它是无效的。我从没见过冲突。所以这还不是一个有效的测试。但现在我们知道了如何对第二个问题给出明确的答案。我们得到了错误的答案,但是我们现在可以修补测试,以生成我们正在寻找的失败。我就是这样做的:我刚刚连续100次做了测试。

    for (int j=0; j<100; ++j) {
      testForThreadClash();
    }
    

    现在我的测试在大约20次迭代中失败了。这证实了我的测试是有效的。我现在可以恢复synchronized关键字并重新运行测试,确信它会告诉我类是否是threadsafe。

        4
  •  1
  •   Phil    17 年前

    在某些情况下,我发现可以通过使用多个相互同步的线程(可能使用countdownloach或某种类似的并发机制),强制对一个潜在的非线程安全类的调用进行特定的问题交织。但是,有时这不起作用,例如,如果您试图测试两个线程同时处于同一方法中时会发生什么。

    这是一篇有趣的文章(但对该工具了解不多): http://today.java.net/pub/a/today/2003/08/06/multithreadedTests.html

        5
  •  0
  •   Jonathan    10 年前

    如果线程是您测试的代码的重点(考虑并发数据结构),那么单元测试多线程代码没有任何错误。一般的方法是阻塞主测试线程,在单独的线程中执行断言,在主线程中捕获并重新抛出任何失败的断言,否则就取消阻塞主测试线程,以便完成测试。这很简单 ConcurrentUnit :

    @Test
    public void shouldDeliverMessage() throws Throwable {
      final Waiter waiter = new Waiter();
    
      messageBus.registerHandler(message -> {
        // Called on separate thread
        waiter.assertEquals(message, "foo");
    
        // Unblocks the waiter.await call
        waiter.resume();
      };
    
      messageBus.send("foo");
    
      // Wait for resume() to be called
      waiter.await(1000);
    }
    

    这里的关键是,任何线程中的任何失败断言都将由 waiter 允许测试通过或失败。