一、背景
本来想借助redis实现一个自增序列,并模拟一下多线程场景下生成的序列是否会出错,代码如下:
@Test
public void test2(){
int count = 10;
for (int i = 0; i < count; i++) {
Runnable target = () -> {
Jedis jedis = jedisPool.getResource();
Long num = jedis.incr("test");
System.out.println("Thread: " + Thread.currentThread().getName() + " || num=" + num);
jedis.close();
};
Thread thread = new Thread(target, "Thread" + i);
thread.start();
}
System.out.println("END");
}
哪想输出的结果并不如预期,Thread.run()中的方法并未执行,整个方法就结束了
为什么会出现这种情况?
二、解决方法
测试方法:
@Test
public void test4() {
Thread thread = new Thread(() -> {
try {
Thread.sleep(100);
System.out.println("子线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
System.out.println("main方法结束");
}
//输出:main方法结束
核心问题在于:在子线程结束执行前并未将主线程阻塞,主线程结束后,jvm直接退出
2.1 使用main函数
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(100);
System.out.println("子线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
System.out.println("main方法结束");
}
2.2 使用countDownLatch
@Test
public void test5() throws Exception{
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread thread = new Thread(() -> {
try {
Thread.sleep(100);
System.out.println("子线程结束");
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
System.out.println("main方法结束");
countDownLatch.await();
}
//main方法结束
//子线程结束
2.3 使用Thread.join
@Test
public void test6() throws Exception{
Thread thread = new Thread(() -> {
try {
Thread.sleep(100);
System.out.println("子线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
System.out.println("main方法结束");
thread.join();
}
//main方法结束
//子线程结束
当然还有其他方法,殊途同归,核心都是保证主线程退出前子线程先结束运行
三、原理
为什么会出现这样的现象,按照我们对守护/非守护线程的理解
非守护线程:我们平常创建的普通线程。
守护线程:用来服务于非守护线程(用户线程);不需要上层逻辑介入(JVM垃圾回收器)
new Thread()创建的默认是非守护线程,
daemon
默认为false,如果要改为守护线程,可以手动在start之前置为true
当main方法结束且只有守护线程存在时,jvm退出
按照上面的例子可以看出,@Test的执行原理和main方法有出入,下面我们采用调用栈大法看看Junit单元测试究竟是怎么执行的
奇怪的是最下面框选的4行调用栈,双击发现没办法进入源码,正常来说是可以点进去的。
通过包名看出应该是idea的jar包,看包名com.intellij.rt.junit
是很重要的线索
既然idea可以加载到这个包,这个jar应该在我们的类路径里,显然应该在idea的安装目录中
进入到idea的安装目录,到lib中没找到,到plugins找找,进入junit/lib
发现这几个很可疑
使用反编译大法,借助jd-gui(下载地址http://java-decompiler.github.io/)反编译一下看看
从调用栈可以看出最先的入口为com.intellij.rt.junit
包中的JunitStarter
@Test执行
test方法运行在主线程中,外层函数执行完test等操作后执行System.exit来退出虚拟机,这个时候thread可能还没执行完,就被销毁了。
Main方法
启动完thread后,主线程结束,但是只有还有普通线程(非守护线程),虚拟机就不会主动退出,而我们也没有写代码让虚拟机退出,因此虚拟机需等待thread运行完毕才退出。因此打印出了子线程结束
。