总览
线程jiggler是一个简单的测试框架,用于执行代码以查找线程问题。 它通过在运行时修改字节码类的类来工作,以在指令之间插入Thread.yield()调用,从而“微动”线程。 这极大地增加了发现线程问题的可能性,并且无需更改生产代码即可做到这一点。
背景
我最近正在研究如何测试多线程代码中的线程问题,并从IBM找到了一个名为ConTest的工具,但找不到我可以使用的任何代码。 很自然地,我以为我会自己加油。
考虑一下这个规范的简单但线程不安全的类:
private int count = 0 ;public void count() {count++;}
count方法的字节码为:
DUP
GETFIELD asm/Foo.counter : I
ICONST_1
IADD
PUTFIELD asm/Foo.counter : I
这提供了几个可以进行上下文切换的位置,这意味着可以增加计数,但未按预期存储。 让我们考虑一个快速的单元测试:
Counter counter = new BadCounter();int n = 1000;@Testpublic void singleThreadedTest() throws Exception {for (int i = 0; i < n; i++) {counter.count();}assertEquals(n, counter.getCount());}...
该测试在单个线程中运行并通过。 让我们尝试在两个线程上运行它,看看它是否失败。
public void threadedTest() throws Exception {final CompletionService<Void> service = new ExecutorCompletionService<Void>(Executors.newFixedThreadPool(2));for (int i = 0; i < n; i++) {service.submit(new Callable<Void>() {@Overridepublic Void call() {counter.count();return null;}});}for (int i = 0; i < n; ++i) {service.take().get();}assertEquals(n, counter.getCount());}
这也过去了。 在我的计算机上,我可以将n增加到100,000,直到它开始持续失败。
Expected :1000000
Actual :999661
只有0.04%的测试有问题。 我们学到了什么? 我们已经学会了一种运行多线程测试的简单方法,但是我们已经知道,因为我们无法控制线程何时执行工作,所以这有点试验和错误。
线程跳动
因此,行使代码来发现线程缺陷的一个问题是您无法控制线程何时屈服。 但是,我们可以重写字节码,以便在指令之间的字节码中插入Thread.yield()。 在上面的示例中,我们可以通过更改字节码来获取产生更多问题的代码:
DUP
GETFIELD asm/Foo.counter : I
INVOKESTATIC java/lang/Thread.yield ()V
ICONST_1
IADD
PUTFIELD asm/Foo.counter : I
使用ASM,我们可以创建一个重写器来插入这些调用。 JigglingClassLoader即时重写类,添加这些调用。 由此,我们可以创建一个JUnit运行器以使用新的类加载器进行测试来运行。
@Jiggle("threadjiggler.test.*")
public class BadCounterTest {...
}
现在运行测试:
Expected :1000000
Actual :836403
我们看到线程问题的测试数量跃升到16%。 我们无需重新编译代码,也不会影响在同一JVM中运行的其他单元测试。
读者练习
SimpleDateFormat是Java中众所周知的非线程安全类。 编写一个使类动摇的测试。 为什么它不是线程安全的? 您将如何重写它以确保线程安全? 您如何在不使用ThreadLocal,锁或同步的情况下这样做?
源代码
可以在Github上找到此代码。
进一步阅读
我写了一篇关于测试线程代码是否正确的文章 。 您可能还希望更一般地阅读:
- 并发的错误模式及其测试方法– Eitan Farchi,Yarden Nir,Shmuel Ur IBM Haifa Research Labs
- 介绍如何测试和调试并发软件的困难的演讲– Shmuel Ur
- Java理论与实践:表征线程安全
翻译自: https://www.javacodegeeks.com/2013/09/thread-jiggling.html