問題起因
有一次要測試下多線程刪除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ù)量,也沒有任何報錯。

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

上面是我寫這篇文章時,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í)行完畢

解決方法
解決方法有幾種,核心目的都是為了讓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í)行完了

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

下面分享下我在找test執(zhí)行流程的過程。
我這個是一個SpringBoot工程,然后每個測試類都會用打上下面2個注解
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ServiceApplication.class)
然后我先找到了這個斷點(diǎn)位置org.springframework.test.context.junit4.SpringJUnit4ClassRunner#run

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

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

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

就是這個類com.intellij.rt.junit.JUnitStarter,但是我在idea里沒有看到相關(guān)的源碼。
后來我又搜了一下,在github上搜到了一個類文件,這個類文件的main方法最后也是執(zhí)行了System.exit()

這個可能是idea自己對junit做了封裝,更多的細(xì)節(jié)我暫時沒有探索到,有知道的也可以評論區(qū)告訴我
后記
總的來說,這是個說大不大,說小不小的問題,如果對junit和jvm的退出機(jī)制了解不是很多的情況下,可能會和我犯一樣的錯誤,浪費(fèi)幾個小時的時間在那debug。
我debug的過程中,甚至debug到了netty的worker關(guān)閉的代碼,我甚至都在懷疑,netty出問題了???
后來實(shí)際證明是我自己想多了。
不過也不能說踩坑不好吧,生活總是在趟過一個又一個或大或小的坑中過去的。
希望大家都能將遇到的每一個坑和挫折踩在腳下,快樂成長