Junit无法正常测试多线程问题原因分析与解决

背景

今天在 Junit 中尝试调试多线程业务,结果发现,testXX 方法中早早就退出了虚拟机,启动的子线程全都压根没有执行,也就是 Junit 中无法正常的测试多线程。

分析

一开始,我以为是在主线程中出现了异常,导致了虚拟机的退出,但是做了一个小实验分析了一下,发现不是那么回事。经过试验,我发现,无论是 Exception RuntimeException 甚至 Error 都不能造成虚拟机的退出,子线程都能被执行到,只有 System.exit(n) 才能直接退掉虚拟机,忽略子线程的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.github.since1986.learn.java;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class LearnThread {

public static void main(String[] args) throws Exception {
int subThreadCount = 9;

ExecutorService executorService = Executors.newFixedThreadPool(subThreadCount);
for (int i = 0; i < subThreadCount; i++) {
executorService.submit(() -> {
System.out.printf("线程 [%s] 运行\n", Thread.currentThread().getName());
});
}

//throw new Exception();
//throw new RuntimeException();
//throw new Error();
System.exit(0);
}
}

输出

1
Process finished with exit code 0

因此,大致可以猜测,Junit 中在什么地方调用了System.exit() 所以造成了子线程无法执行,Google 了一下,有这篇文章也研究了这个问题,果然在 Junit 中有一个类 org.junit.runner.JUnitCore 包含了 System.exit()

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Run the tests contained in the classes named in the <code>args</code>.
* If all tests run successfully, exit with a status of 0. Otherwise exit with a status of 1.
* Write feedback while tests are running and write
* stack traces for all failed tests after the tests all complete.
*
* @param args names of classes in which to find tests to run
*/
public static void main(String... args) {
Result result = new JUnitCore().runMain(new RealSystem(), args);
System.exit(result.wasSuccessful() ? 0 : 1);
}

我想在这个类上打上断点试一下,走一遍,结果惊奇的发现,断点并没有走到这个,然后看了看 debug 窗中线程 main 发现有这么一个类 com.intellij.rt.execution.junit.JUnitStarter 最后 main() 原来是在这了类里的,这个类明显是 idea 内部的一个插件类。

Google 了一下,实际上这个插件是开源的,可以看到这个类的源代码,大致上看了一眼,里面也是调用了System.exit()(不管怎样,最后 testXX 方法肯定会是通过main()来执行的,所以我们看main()就好了),所以,大只能印证个大概,是因为 Junit 当中使用了System.exit(),所以导致了子线程的执行因为虚拟机的退出而被忽略,所以无法正常测试多线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws IOException {
Vector argList = new Vector();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
argList.addElement(arg);
}

final ArrayList listeners = new ArrayList();
final String[] name = new String[1];

String agentName = processParameters(argList, listeners, name);

if (!JUNIT5_RUNNER_NAME.equals(agentName) && !canWorkWithJUnitVersion(System.err, agentName)) {
System.exit(-3);
}
if (!checkVersion(args, System.err)) {
System.exit(-3);
}

String[] array = new String[argList.size()];
argList.copyInto(array);
int exitCode = prepareStreamsAndStart(array, agentName, listeners, name[0]);
System.exit(exitCode);
}

解决

其实明白了大致的原因,解决起来也就简单了,让主线程等待子线程都执行完毕就好了,实现这一点有多种方式,可以用”倒计数门栓”或者”可循环使用屏障”。(实际上我中间请教了我的同事,他并没有分析这莫多,大致凭经验告诉我可以使用倒计数门栓,我当时试了确实管用)

总结

通过这次解决工作中出现的一个问题的契机,我对 Junit 以及多线程又加深了一些理解。总的来说,对于理论的理解要在实践中不断的磨练才能越来越深入。