JUnit测试Runnable线程时循环仅执行一次的问题排查与解决
JUnit测试中多线程Sleep后循环仅执行一次的问题解析与修复
问题描述
我在学习Java多线程时,将参考的Demo代码迁移到JUnit测试中实现(原本是独立Java程序),遇到了一个奇怪的问题:
我定义了两个实现Runnable接口的类,它们的run方法里都有一个循环,期望执行5次,每次间隔0.5秒(用Thread.currentThread().sleep(500)实现)。但实际运行测试时,循环逻辑只执行了一次;移除sleep语句后,循环能正常执行5次。
测试代码如下:
import org.junit.jupiter.api.Test; public class TheadTest { @Test void Threadtest2() { Hi hi = new Hi(); Hello hello = new Hello(); Thread t1 = new Thread(hi); Thread t2 = new Thread(hello); t1.start(); t2.start(); } } class Hi implements Runnable { public void runx() {} public void run() { for (int i = 0; i < 5; i++) { System.out.println("HI"); System.out.println(i); try { Thread.currentThread().sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Hello implements Runnable { public void runx() {} public void run() { for (int j = 0; j < 5; j++) { System.out.println("Hello"); try { System.out.println(j); Thread.currentThread().sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
问题原因
核心问题出在JUnit测试的生命周期机制上:
- JUnit测试方法(这里的
Threadtest2)是在一个主线程中执行的,当这个方法执行完最后一行代码(t2.start())后,主线程就会结束。 - JUnit测试框架默认不会等待测试方法中启动的后台线程完成,一旦测试方法执行完毕,测试进程就会开始终止流程,JVM会强制终止所有还在运行的非守护线程(你的
t1和t2都是非守护线程)。 - 当你添加
sleep(500)后,子线程刚执行完第一次循环就进入休眠状态,此时测试主线程已经结束,测试进程随之终止,子线程还没来得及执行剩下的4次循环就被杀死了;而移除sleep时,子线程的循环执行速度极快,在测试进程终止前就完成了5次循环,所以看起来正常。
另外补充一点:Thread.currentThread().sleep(500)和Thread.sleep(500)效果完全一样,因为sleep是静态方法,无论用哪个线程对象调用,都是让当前执行的线程休眠,这里写法可以简化为Thread.sleep(500)。
修复方法
解决思路很简单:让测试主线程等待两个子线程全部执行完成后再结束,常用的方式有两种:
方法1:使用Thread.join()
在测试方法中启动线程后,调用t1.join()和t2.join(),这会让测试主线程阻塞,直到对应的子线程执行完毕:
import org.junit.jupiter.api.Test; public class TheadTest { @Test void Threadtest2() throws InterruptedException { // 需要声明抛出InterruptedException Hi hi = new Hi(); Hello hello = new Hello(); Thread t1 = new Thread(hi); Thread t2 = new Thread(hello); t1.start(); t2.start(); // 等待两个子线程完成 t1.join(); t2.join(); } } // Hi和Hello类代码不变
方法2:使用CountDownLatch(更适合多线程场景扩展)
如果后续需要等待更多线程,CountDownLatch会更灵活:
import org.junit.jupiter.api.Test; import java.util.concurrent.CountDownLatch; public class TheadTest { @Test void Threadtest2() throws InterruptedException { CountDownLatch latch = new CountDownLatch(2); // 计数器初始值为2,对应两个线程 Hi hi = new Hi(latch); Hello hello = new Hello(latch); Thread t1 = new Thread(hi); Thread t2 = new Thread(hello); t1.start(); t2.start(); latch.await(); // 等待计数器归0 } } class Hi implements Runnable { private CountDownLatch latch; public Hi(CountDownLatch latch) { this.latch = latch; } public void runx() {} public void run() { try { for (int i = 0; i < 5; i++) { System.out.println("HI"); System.out.println(i); Thread.sleep(500); } } catch (InterruptedException e) { e.printStackTrace(); } finally { latch.countDown(); // 线程完成,计数器减1 } } } class Hello implements Runnable { private CountDownLatch latch; public Hello(CountDownLatch latch) { this.latch = latch; } public void runx() {} public void run() { try { for (int j = 0; j < 5; j++) { System.out.println("Hello"); System.out.println(j); Thread.sleep(500); } } catch (InterruptedException e) { e.printStackTrace(); } finally { latch.countDown(); // 线程完成,计数器减1 } } }
两种方法都能保证测试主线程等待子线程完成所有循环后再结束,这样sleep后的循环就能正常执行5次了。
内容的提问来源于stack exchange,提问作者RAY
相关产品推荐
相关产品推荐

