junit多線程測試異常

問題起因

有一次要測試下多線程刪除redis數(shù)據(jù),會不會有問題,比如邊界條件沒控制好啥的,導(dǎo)致多刪數(shù)據(jù)了,我就想著寫一個測試用例,使用線程池去模擬多線程刪除的場景。

原以為是個很簡單的場景,結(jié)果多線程咋整都有問題。

下面上個對比的代碼,單線程和多線程訪問執(zhí)行,以redis查詢?yōu)槔?/p>

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.zto.titans.zim.service.ServiceApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ServiceApplication.class)
public class BigKeysTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private final String key = "test_set_add_4";

    private final ThreadPoolExecutor CLEAN_TASK_EXECUTOR = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10), new ThreadFactoryBuilder().setNameFormat("sorted-set-clean-task-%d").build());
  
  
  @Test
    public void testCard() {
        Long count = redisTemplate.opsForZSet().zCard(key);
        System.out.println(count);
    }


    @Test
    public void multiCard() throws InterruptedException {
        for (int i = 0; i < 2; i++) {
            CLEAN_TASK_EXECUTOR.execute(() -> {
                try {
                    Long countOfZset = redisTemplate.opsForZSet().zCard(key);
                    System.out.println(countOfZset);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        System.out.println("end");
    }

    @Test
    public void testAddAndGet() {
        redisTemplate.opsForZSet().zCard(key);
        
    }
}

分別執(zhí)行testCard()和multiCard 2個用例,會發(fā)現(xiàn)testCard會順利打印zset key的元素數(shù)量,而multiCard只會打印一個end,也不會打印數(shù)量,也沒有任何報錯。


image.png

斷點(diǎn)執(zhí)行,可以獲取到錯誤


image.png

上面是我寫這篇文章時,debug獲得的錯誤,我第一次遇到的錯誤信息是這個,無法從連接池獲取連接,給我一頓折磨

org.springframework.data.redis.connection.PoolException:
 Could not get a resource from the pool; 
nested exception is io.lettuce.core.RedisException: 
Cannot retrieve initial cluster partitions from initial URIs 
[RedisURI [host='10.7.100.100', port=6381], 
RedisURI [host='10.7.100.101', port=6381],
 RedisURI [host='10.7.100.101', port=6382], 
RedisURI [host='10.7.100.100', port=6382]]

問題分析

一開始我以為是我的redis cluster出現(xiàn)了問題,但是試了好幾次testCard()方法都沒問題,后來我就去搜了一下,然后發(fā)現(xiàn)junit執(zhí)行完會執(zhí)行直接退出,而不是等到所有線程都執(zhí)行完才會退出

再來復(fù)習(xí)下jvm退出的條件,一般有2個條件

  • jvm中的所有線程都是守護(hù)線程
  • 調(diào)用System.exit()執(zhí)行退出

junit就是在main方法執(zhí)行完之后調(diào)用了System.exit()直接退出了,而不會等待子線程執(zhí)行完畢


image.png

解決方法

解決方法有幾種,核心目的都是為了讓main線程等子線程執(zhí)行完再退出,有下面幾種方法

  • Thread.sleep(1000),讓main線程睡一會,子線程得以執(zhí)行
  • Thread.join(),不過join的話不能用線程池,自己new Thread(),獲取線程的引用
  • 使用CountDownLatch,等子線程執(zhí)行完了再去執(zhí)行main線程

關(guān)于Thread.join(),由于我是用的線程池,沒有手動new Thread(),不知道線程池有沒有辦法做join,有知道的可以評論區(qū)告訴我一下。

我是用的CountDownLatch,給一下我的CountDownLatch的示例

@Test
public void multiCard() {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        for (int i = 0; i < 2; i++) {
            CLEAN_TASK_EXECUTOR.execute(() -> {
                try {
                    Long countOfZset = redisTemplate.opsForZSet().zCard(key);
                    System.out.println("zset count: " + countOfZset);
                    countDownLatch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        try {
            countDownLatch.await(5, TimeUnit.SECONDS);
            System.out.println("count down end");
        } catch (InterruptedException e) {
            //ignore
        }
        System.out.println("end");
}

看控制臺輸出,2個子線程正常執(zhí)行完了


image.png

號外

我第一次遇到這個問題的時候,當(dāng)天就根據(jù)junit+多線程搜到了原因,就是junit的main方法執(zhí)行完會退出。
我出于好奇,又跟了一下,想看看哪個地方執(zhí)行的退出的,上面貼過的這張圖,debug的時候根本就沒走這里

image.png

下面分享下我在找test執(zhí)行流程的過程。

我這個是一個SpringBoot工程,然后每個測試類都會用打上下面2個注解

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ServiceApplication.class)

然后我先找到了這個斷點(diǎn)位置org.springframework.test.context.junit4.SpringJUnit4ClassRunner#run

image.png

然后順著這個方法往上找,上層調(diào)用位置是org.junit.runner.JUnitCore#run(org.junit.runner.Runner)

image.png

但是順著JUnitCore#run往上找調(diào)用的時候,發(fā)現(xiàn)上層調(diào)用的2個地方斷點(diǎn)根本都不會進(jìn)


image.png

然后我在debug的時候,發(fā)現(xiàn)有一個往前的按鈕,點(diǎn)擊這個按鈕就可以回到最最開始的入口


image.png

就是這個類com.intellij.rt.junit.JUnitStarter,但是我在idea里沒有看到相關(guān)的源碼。

后來我又搜了一下,在github上搜到了一個類文件,這個類文件的main方法最后也是執(zhí)行了System.exit()


image.png

github類文件地址

這個可能是idea自己對junit做了封裝,更多的細(xì)節(jié)我暫時沒有探索到,有知道的也可以評論區(qū)告訴我

后記

總的來說,這是個說大不大,說小不小的問題,如果對junit和jvm的退出機(jī)制了解不是很多的情況下,可能會和我犯一樣的錯誤,浪費(fèi)幾個小時的時間在那debug。

我debug的過程中,甚至debug到了netty的worker關(guān)閉的代碼,我甚至都在懷疑,netty出問題了???

后來實(shí)際證明是我自己想多了。

不過也不能說踩坑不好吧,生活總是在趟過一個又一個或大或小的坑中過去的。

希望大家都能將遇到的每一個坑和挫折踩在腳下,快樂成長

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容