用JUnit测试多线程代码的新方法


当前,当我们测试多线程Java时,我们通过尽可能多的线程来调用被测类。而且由于测试不是确定性的,因此我们会尽可能重复进行此测试。

这种方法的缺点是,大多数时候我们的错误测试都成功了,这使得调试多线程错误成为一场噩梦。因此,我开发了一个开源工具vmlens来确定多线程Java的JUnit测试。并使调试更容易。

这个想法是为给定的测试执行所有可能的线程交织。并报告失败的线程交织,这使调试成为可能。

A Test for a Concurrent Counter 以下示例显示如何使用vmlens为并发计数器编写测试。所有测试都在package的GitHub项目vmlens-examples中。 com.vmlens.examples.tutorial.counter

import com.vmlens.api.AllInterleavings;
public class TestCounterNonVolatile {
 int i = 0;
 @Test
 public void test() throws InterruptedException {
   try (AllInterleavings allInterleavings = 
     new AllInterleavings
        ("tutorial.counter.TestCounterNonVolatile");) {
     while (allInterleavings.hasNext()) {
      i = 0;
      Thread first = new Thread(() -> {
        i++;
      });
      Thread second = new Thread(() -> {
        i++;
      });
      first.start();
      second.start();

      first.join();
      second.join();

      assertEquals(2,i);
     }
   }
 }
}

我们i从两个线程增加字段。并且在两个线程都完成之后,我们检查计数是否为2。诀窍是,通过使用class遍历所有线程交织的while循环来包围完整的测试AllInterleavings

vmlens作为Java代理运行,并使用字节码转换来计算所有线程交织。因此,您需要按如下所述在Maven Pom中配置vmlens 。运行测试后,我们可以在target / interleave / elements.html文件中的interleave报告中查看所有测试运行的结果。

interleave.png

我们的测试(名称为tutorial.counter.TestCounterVolatile的测试5)由于数据争用而失败。数据竞争意味着对共享字段的读取和写入未正确同步。JIT编译器或CPU可以对不正确同步的读写进行重新排序。这里可以 很重要。通常,错误同步的读写会返回正确的结果。仅在非常特殊的情况下,通常将特定的CPU体系结构,特定的JVM和特定的线程交织在一起才能导致错误的值。

vmlens检查每个字段访问是否正确同步以检测数据竞争。

并发挥发性计数器的测试 为了解决数据争用问题,我们将字段声明为易失性:

public class TestCounterVolatile {
 volatile int i = 0;
 @Test
 public void test() throws InterruptedException {
   try (AllInterleavings allInterleavings = 
      new AllInterleavings
        ("tutorial.counter.TestCounterVolatile");) {
      while (allInterleavings.hasNext()) {
       i = 0;
       Thread first = new Thread(() -> {
         i++;
       });
       Thread second = new Thread(() -> {
         i++;
       });
       first.start();
       second.start();

       first.join();
       second.join();

       assertEquals(2,i);
      }
   }
 }
}

这样可以解决数据争用,但是现在断言失败了:

TestCounterVolatile.test:22 expected:<2> but was:<1>

要查看出了什么问题,请在交错报告中单击测试tutorial.counter.TestCounterVolatile。这向我们展示了错误的交错:

volatilecounter.png

错误是两个线程都先读取变量i,然后再更新变量。因此,第二个线程将覆盖第一个线程的值。

A Test With an Atomic Counter 要编写正确的并发计数器,我们使用类AtomicInteger:

public class TestCounterAtomic {
 AtomicInteger i = new AtomicInteger();
 @Test
 public void test() throws InterruptedException {
   try (AllInterleavings allInterleavings = 
      new AllInterleavings
         ("tutorial.counter.TestCounterAtomic");) {
      while (allInterleavings.hasNext()) {
       i.set(0);
       Thread first = new Thread(() -> {
        i.incrementAndGet();
       });
       Thread second = new Thread(() -> {
        i.incrementAndGet();
       });
       first.start();
       second.start();

       first.join();
       second.join();
       assertEquals(2,i.get());
      }
   }
  }
}

现在我们的计数器的增量是原子的,我们的测试终于成功了。

结论

如我们所见,执行多线程测试的所有线程交织使多线程测试具有确定性。而且它使调试失败的测试成为可能。为了测试所有线程交织,我们使用一个while循环对测试进行了包围,该循环使用class遍历了所有线程交织AllInterleavings。vmlens使用字节码转换来计算所有线程交织。因此,您还需要按如下所述在Maven Pom中配置vmlens。如果测试失败,您可以查看发生故障的线程交错调试我们的测试。


原文链接:http://codingdict.com