線程池阻塞問題

問題

記錄一下生產(chǎn)環(huán)境出現(xiàn)的問題禀梳。。利耍。

幾天生產(chǎn)環(huán)境有同事反映分頁查詢一直在轉(zhuǎn)圈查不出來數(shù)據(jù)蚌本,跟我反饋,我也是很積極的去看有什么問題隘梨,我以為就是比較常見的問題吧程癌,當(dāng)我看的時(shí)候覺得很奇怪。

有一個(gè)分頁的接口其實(shí)有很多的日志需要打印轴猎,為什么只打印了一點(diǎn)日志就沒有后續(xù)了嵌莉,然后前臺(tái)頁面一直在轉(zhuǎn)圈圈等待數(shù)據(jù)的返回,怎么滴捻脖?是不喜歡下面的代碼不想執(zhí)行么锐峭,很顯然不是中鼠。

應(yīng)該正確打印的日志:

20230228162624.png

而實(shí)際生產(chǎn)上面打印到下面這條日志結(jié)束了。

查詢字典信息沿癞,返回?cái)?shù)據(jù): ······

那是什么情況呢援雇?

首先我們說明一下出現(xiàn)問題的場景,emm其實(shí)就是一個(gè)分頁查詢椎扬。但是呢惫搏,分頁的數(shù)據(jù)需要查詢一些其他的數(shù)據(jù),組裝以后返回給前端頁面蚕涤。

簡單點(diǎn)就是這樣子:

系統(tǒng)調(diào)用簡圖
  1. 前端發(fā)起請求查詢數(shù)據(jù)
  2. 后端根據(jù)查詢到的數(shù)據(jù)筐赔,組裝后請求三方接口(三個(gè)接口)
  3. 三方接口返回?cái)?shù)據(jù)給后端服務(wù)
  4. 后端服務(wù)請求完成后返回給頁面

我們現(xiàn)在將后端服務(wù)詳細(xì)的執(zhí)行過程表述下

代碼流轉(zhuǎn)圖
  1. 查詢到分頁數(shù)據(jù)后,循環(huán)每條數(shù)據(jù)钻趋,使用多線程進(jìn)行查詢?nèi)浇涌冢ǘ嗑€程交給線程池執(zhí)行)
  2. 每個(gè)數(shù)據(jù)的線程在查詢數(shù)據(jù)時(shí)有分了三個(gè)線程去查詢數(shù)據(jù)(同樣交給多線程)川陆,數(shù)據(jù)的線程等待查詢的線程相應(yīng)結(jié)果才能往下執(zhí)行
  3. 查詢返回的結(jié)果組裝后返回

正文

下面看下代碼時(shí)怎么寫的。蛮位。较沪。

public PageUtils queryPage(Map<String, Object> params) {
    //查詢分頁數(shù)據(jù)
    Page page = new Page((Integer) params.get(Constant.PAGE),
            (Integer) params.get(Constant.SIZE));
    IPage<FlowCardInfoDto> iPage = this.baseMapper.getPage(page, params);
    List<FlowCardInfoDto> records = iPage.getRecords();
    if (records.isEmpty()) {
        return new PageUtils(iPage);
    }
    //CountDownLatch  等待所有結(jié)果返回才進(jìn)行下一步  size=10
    CountDownLatch latch = new CountDownLatch(records.size());
    //查詢卡商對(duì)象關(guān)系---這里打印:查詢字典信息失仁,返回?cái)?shù)據(jù): ······
    Map<String, Object> dealBeanMap = dictUtil.getDealBeanMap("dealer_type");
    List<FlowCardInfoDto> collect = records.stream().peek(item -> {
        //查詢實(shí)時(shí)流量
        String dealerBeanName = (String) dealBeanMap.get(item.getDealerNo());
        if (StringUtils.isNotBlank(dealerBeanName)) {
            Runnable runnable = () -> {
                //有此卡商
                try {
                    //*****省略部分代碼*****
                    //這里根據(jù)分頁的數(shù)據(jù): iccId查詢?nèi)浇涌跀?shù)據(jù)
                    trafficMap = strategy.trafficQueryDoPost(trafficMap);
                    //*****省略部分代碼*****
                } finally {
                    //CountDownLatch 執(zhí)行完遞減
                    latch.countDown();
                }
            };
            //這里將 runnable 交給 flowCardThreadPoolExecutor 線程池
            flowCardThreadPoolExecutor.execute(runnable);
        } else {
            log.info("卡商<{}>處理接口未配置J!萄焦!", dealerBeanName);
            latch.countDown();
        }
    }).collect(Collectors.toList());
    try {
        latch.await();
        log.info("====================================");
    } catch (Exception e) {
        log.error("卡商查詢主線程等待異常", e);
    }
    iPage.setRecords(collect);
    return new PageUtils(iPage);
}

/**
 * 根據(jù) iccId查詢?nèi)綌?shù)據(jù)
 * @return Map
 */
protected Map<String, Object> trafficQueryDoPost(Map<String, Object> param) {
    log.info("開始查詢數(shù)據(jù)");
    Map<String, Object> cardInfoMap = new HashMap<>();
    cardInfoMap.put("cardNumber", param.get("iccId"));
    CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
        //查詢數(shù)據(jù)一交給線程池 flowCardThreadPoolExecutor控轿,http請求
    }, flowCardThreadPoolExecutor).whenComplete((object, throwable) -> cardInfoMap.put("cardInfo", object));
    
    CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> {
        //查詢數(shù)據(jù)二交給線程池 flowCardThreadPoolExecutor,http請求
    }, flowCardThreadPoolExecutor).whenComplete((object, throwable) -> cardInfoMap.put("dailyFlow", object));

    CompletableFuture<Object> future3 = CompletableFuture.supplyAsync(() -> {
        //查詢數(shù)據(jù)三交給線程池 flowCardThreadPoolExecutor拂封,http請求
    }, flowCardThreadPoolExecutor).whenComplete((object, throwable) -> cardInfoMap.put("monthlyFlow", object));
    //等待所有結(jié)果的
    CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2, future3);

    //阻塞茬射,直到所有任務(wù)結(jié)束。
    log.info("**:等待所有結(jié)果返回");
    all.join();
    log.info("**:所有結(jié)果全部返回");
    return cardInfoMap;
}

/**
 * 線程池配置
 * @return Map
 */
@Bean("flowCardThreadPoolExecutor")
public ThreadPoolTaskExecutor flowCardThreadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(20);
    executor.setQueueCapacity(100);
    executor.setKeepAliveSeconds(60);
    executor.setThreadNamePrefix(threadNamePrefix);

    executor.setRejectedExecutionHandler((r, executor1) -> {
        if (!executor1.isShutdown()) {
            try {
                executor1.getQueue().put(r);
            } catch (InterruptedException e) {
                log.error("interruptedException:{}", e.toString());
            }
        }
    });
    // 初始化
    executor.initialize();
    return executor;
}

上面的代碼展示了分頁查詢的大體邏輯冒签,每次分頁的數(shù)據(jù)條數(shù)默認(rèn)十條帶有 iccId 的數(shù)據(jù)在抛,每條 iccId 都會(huì)開辟一個(gè)線程來查流量,而這個(gè)線程交給線程池萧恕,然后每個(gè)線程查詢流量時(shí)刚梭,開辟三個(gè)線程去查詢流量,同樣交給了同一個(gè)線程池票唆,流量返回后組裝完成朴读,一條 iccId 卡才算執(zhí)行完成進(jìn)行 countDown()

問題就出在了線程池上面走趋,我們可以想一下衅金,有關(guān)線程池的線程沒有日志時(shí)怎么回事,沒有執(zhí)行嗎?是的氮唯,它就是沒有執(zhí)行酥宴。

分頁查詢的十條數(shù)據(jù)開辟的線程池交給了線程池,瞬間就占用了僅有的十個(gè)核心線程您觉,而這十個(gè)核心線程每個(gè)都必須等待自己開辟的三個(gè)核心線程都有結(jié)果后才能釋放資源,但是這三個(gè)線程都在隊(duì)列里面無法執(zhí)行(隊(duì)列未滿時(shí)授滓,只有核心線程在工作琳水,此時(shí)沒有普通線程數(shù)),造成了查詢流量的三十個(gè)線程(三個(gè)查詢流量的線程 * 十條 iccId )壓根就沒有機(jī)會(huì)執(zhí)行般堆,而核心線程又在等待它們的結(jié)果 all.join(); 一直等待 在孝。

造成的結(jié)果就是系統(tǒng)服務(wù)中凡是涉及到交給線程池執(zhí)行的操作都不能正常執(zhí)行。

改進(jìn)

  • 順序執(zhí)行:將查詢流量的三個(gè)三方接口順序執(zhí)行淮摔,不依靠多線程和線程池私沮。缺點(diǎn)就是時(shí)間長,效率低和橙,頁面使用的人可能要罵人仔燕,服務(wù)間調(diào)用可能會(huì)超時(shí)(既然能順序執(zhí)行你猜我為什么要用多線程?)

  • 線程隔離:另起一個(gè)線程配置魔招,將分頁數(shù)據(jù)的線程依舊交給原來的線程池 flowCardThreadPoolExecutor 晰搀,將查詢流量的三條線程交給另外一個(gè)線程池配置,使得兩個(gè)線程互不影響办斑,查詢流量的線程始終有機(jī)會(huì)執(zhí)行外恕,就不會(huì)造成 flowCardThreadPoolExecutor 線程池阻塞。

保險(xiǎn)起見乡翅,將查詢流量的多線程操作進(jìn)行如下改動(dòng)鳞疲,設(shè)置取值的最大等待時(shí)間,超過時(shí)間拋棄此次請求蠕蚜,保證數(shù)據(jù)的線程不受影響尚洽。

CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2, future3);

//阻塞,直到所有任務(wù)結(jié)束波势。
log.info("**:等待所有結(jié)果返回");
try {
    all.get(10, TimeUnit.SECONDS);
} catch (Exception e) {
    log.error("**:等待結(jié)果返回異常:", e);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末翎朱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子尺铣,更是在濱河造成了極大的恐慌拴曲,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凛忿,死亡現(xiàn)場離奇詭異澈灼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門叁熔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來委乌,“玉大人,你說我怎么就攤上這事荣回≡饷常” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵心软,是天一觀的道長壕吹。 經(jīng)常有香客問我,道長删铃,這世上最難降的妖魔是什么耳贬? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮猎唁,結(jié)果婚禮上咒劲,老公的妹妹穿的比我還像新娘。我一直安慰自己诫隅,他們只是感情好腐魂,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阎肝,像睡著了一般挤渔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上风题,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天判导,我揣著相機(jī)與錄音,去河邊找鬼沛硅。 笑死眼刃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摇肌。 我是一名探鬼主播擂红,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼围小!你這毒婦竟也來了昵骤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤肯适,失蹤者是張志新(化名)和其女友劉穎变秦,沒想到半個(gè)月后欺旧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羽历,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡礼患,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了痴柔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咸灿。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡郑趁,死狀恐怖齐饮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情福贞,我是刑警寧澤撩嚼,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站挖帘,受9級(jí)特大地震影響绢馍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜肠套,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猖任。 院中可真熱鬧你稚,春花似錦、人聲如沸朱躺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽长搀。三九已至宇弛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間源请,已是汗流浹背枪芒。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谁尸,地道東北人舅踪。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像良蛮,于是被迫代替她去往敵國和親抽碌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 前段時(shí)間我們接入了 ELK 公司出品的 Elastic-APM 作為全鏈路監(jiān)控平臺(tái)决瞳,終結(jié)了我好幾年前擼的字節(jié)碼注入...
    Java弟中弟閱讀 1,361評(píng)論 0 1
  • 1.進(jìn)程與線程的概念货徙,以及為什么要有進(jìn)程線程,其中有什么區(qū)別皮胡,他們各自又是怎么同步的 基本概念:進(jìn)程是對(duì)運(yùn)行時(shí)程序...
    某WAP閱讀 649評(píng)論 0 0
  • 思考 你是否有此疑問:普通線程使用后即銷毀痴颊,而對(duì)于線程池中核心線程將一直存在,非核心線程會(huì)銷毀胸囱,它是如何做到的祷舀?看...
    唯愛_0834閱讀 451評(píng)論 0 2
  • 在日常開發(fā)中,線程池是使用非常頻繁的一種技術(shù),無論是服務(wù)端多線程接收用戶請求裳扯,還是客戶端多線程處理數(shù)據(jù)抛丽,都會(huì)用到線...
    和帥_db6a閱讀 693評(píng)論 0 0
  • 隊(duì)列 說阻塞隊(duì)列之前先要明白什么是隊(duì)列?隊(duì)列是一種特殊的線性表饰豺,在隊(duì)列中插入一個(gè)隊(duì)列元素稱為入隊(duì)亿鲜,從隊(duì)列中刪除一個(gè)...
    fit_All閱讀 610評(píng)論 0 0