一、背景
今天@段段提出了一个很好的问题,她发现单元测试时如果开多个线程,主线程运行结束就结束了,并不会等待子线程结束。
如果用main方法就没问题,技术群里展开了激烈的讨论。
本文将“复现”这种现象,并给出多种解决方案,并纠正个别文章的错误,并追到源头带大家找出问题的原因。
本文会分享几点干货,如调用栈大法、发编译大法等。
二、复现场景的源码
我们的线程类
代码语言:javascript复制public class DemoThread extends Thread {
private static int num = 0;
public DemoThread() {
}
@Override
public void run() {
synchronized (DemoThread.class) {
for (int i = 0; i < 4; i ) {
System.out.println(Thread.currentThread().getName() "-->" num );
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static int getNum() {
return num;
}
}
测试代码
代码语言:javascript复制public class ThreadDemoTest {
@Test
public void test(){
DemoThread thread1 = new DemoThread();
DemoThread thread2 = new DemoThread();
thread1.start();
thread2.start();
}
}
运行
和预期完全不符,预期应该是 线程1输出0-3,线程2输出4-7,结果就输出一个,什么情况??
三、解决方案
所有的解决方法的核心是:在子线程没结束之前让主线程阻塞住。
3.1 使用main函数
代码语言:javascript复制public class ThreadDemoTest {
public static void main(String[] args) {
DemoThread thread1 = new DemoThread();
DemoThread thread2 = new DemoThread();
thread1.start();
thread2.start();
}
}
结果
发现符合预期
3.2 CountDownLatch
我们的线程类:
代码语言:javascript复制public class DemoThread extends Thread {
private CountDownLatch latch;
private static int num = 0;
public DemoThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
synchronized (DemoThread.class) {
for (int i = 0; i < 4; i ) {
System.out.println(Thread.currentThread().getName() "-->" num );
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
latch.countDown();
}
}
public static int getNum() {
return num;
}
}
我们的单元测试类
代码语言:javascript复制public class ThreadDemoTest {
@Test
public void test() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
DemoThread thread1 = new DemoThread(latch);
DemoThread thread2 = new DemoThread(latch);
thread1.start();
thread2.start();
latch.await();
}
}
结果依然符合预期
3.3 使用join函数
代码语言:javascript复制public class ThreadDemoTest {
@Test
public void test() throws InterruptedException{
DemoThread thread1 = new DemoThread();
DemoThread thread2 = new DemoThread();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
结果依然符合预期
当然还有其他办法,殊途同归。
四、研究
有一些人对这种现象进行了研究如《Junit单元测试不支持多线程测试--原因分析和问题解决》,很有道理。
但是最起码在Idea里发现有出入。
但是在其中提到的TestRunner各个退出虚拟机的地方断点,整个运行过程中没有一个断点停下来。
上调用栈大法:
我们发现主函数的入口是在Idea的jar包中。
我们双击调用栈这里,发现进不去源码(正常是可以双击进去的)。
肿么办?难道就此放弃??应该不是我们的风格。
皮皮虾我们走,去找源码去。
在网上搜了半天没找到,囧....
然后,我们就此放弃??不是我们的风格。
看这里包名:com.intellij.rt.execution.junit 这是一个很重要的线索
既然Idea可以加载到这个类,这个Jar包应该在我们的类路径里,而包名就是intellij显然应该在Idea的安装目录中。
隐藏的很深,去lib找一下,没有,去plugins找一下,
进去,发现这几个很可疑
上Java的jar包反编译工具大法
把这几个jar包都放到jd-gui(下载地址:http://java-decompiler.github.io/)中反编译一下看看,发现junit.jar里就有我们想找的入口
其中system.exit这里是关键,另外属性里看到的值和上面断点。
感兴趣可以一层一层直接跟到Junit4的源码里看看,入口在这里:
org.junit.runner.JUnitCore#run(org.junit.runner.Runner)
原理就是如果不主动阻塞,主线程停止后,main方法会调用System.exit退出虚拟机,如果子线程耗时较长,导致子线程没执行完就销毁了。
即test方法运行在主线程中,外层函数执行完test等操作后执行System.exit来退出虚拟机,这个时候thread1和thread2可能还没执行完,就被销毁了。
而使用main方法
启动完thread2后,主线程结束,但是只有还有普通线程(非守护线程),虚拟机就不会主动退出,而我们也没有写代码让虚拟机退出,因此虚拟机需等待thread1和thread2运行完毕才退出。因此打印出了0-7。
其实即使使用main函数如果我们最后加上退出虚拟机的命令效果也是一样,依然是子线程没执行完虚拟机退出:
另外回头看
另外我们看顶层的main函数,参数包含了junit版本,和测试类和测试方法,作为运行时参数传给JUnitStarter的main函数。
五、Learn More
通过上面的分析我们不仅要了解到为什么单元测试时,主线程结束就结束了而不等待子线程。
更重要的是我们要掌握常见的调试和研究源码的方法,如调用栈大法,反编译jar包大法。
通过查看调用栈,可以了解整个调用的全貌,可以去查看上层大调用代码,这对学习源码、熟悉某个框架有极大的帮助。
当我们拉取不到源码或者项目里暂时不需要加入这个jar包时,可以直接下载jar包拖到反编译工具中去看。
六、总结
我们不要轻易的相信网上的各种文章,如果有时间要自己去写个demo代码去断点调试一下,实践是检验真理的标准!
这点和《几个本地搭建练习项目来学习的小技巧分享》所提到的非常一致,我们可以在本地创建demo项目去研究和学习源码等。
另外我们不仅要思考问题,还要写代码去印证自己的想法,当要诡异的现象时,要及时研究其原因,这是你深入学习某个知识点的最好机会之一。