成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

一次 HashSet 所引起的并發(fā)問題

fjcgreat / 3528人閱讀

摘要:打開郵件一看,果然告知我有一個應(yīng)用的線程池隊列達到閾值觸發(fā)了報警。線程池的名稱一定得取的有意義,不然是自己給自己增加難度。根據(jù)監(jiān)控將線程池的隊列大小調(diào)整為一個具體值,并且要有拒絕策略。

背景

上午剛到公司,準備開始一天的摸魚之旅時突然收到了一封監(jiān)控中心的郵件。

心中暗道不好,因為監(jiān)控系統(tǒng)從來不會告訴我應(yīng)用完美無 bug,其實系統(tǒng)挺猥瑣。

打開郵件一看,果然告知我有一個應(yīng)用的線程池隊列達到閾值觸發(fā)了報警。

由于這個應(yīng)用出問題非常影響用戶體驗;于是立馬讓運維保留現(xiàn)場 dump 線程和內(nèi)存同時重啟應(yīng)用,還好重啟之后恢復(fù)正常。于是開始著手排查問題。

分析

首先了解下這個應(yīng)用大概是做什么的。

簡單來說就是從 MQ 中取出數(shù)據(jù)然后丟到后面的業(yè)務(wù)線程池中做具體的業(yè)務(wù)處理。

而報警的隊列正好就是這個線程池的隊列。

跟蹤代碼發(fā)現(xiàn)構(gòu)建線程池的方式如下:

ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize,
              0L, TimeUnit.MILLISECONDS,
              new LinkedBlockingQueue());;
             put(poolName,executor);

采用的是默認的 LinkedBlockingQueue 并沒有指定大小(這也是個坑),于是這個隊列的默認大小為 Integer.MAX_VALUE。

由于應(yīng)用已經(jīng)重啟,只能從僅存的線程快照和內(nèi)存快照進行分析。

內(nèi)存分析

先利用 MAT 分析了內(nèi)存,的到了如下報告。

其中有兩個比較大的對象,一個就是之前線程池存放任務(wù)的 LinkedBlockingQueue,還有一個則是 HashSet。

當然其中隊列占用了大量的內(nèi)存,所以優(yōu)先查看,HashSet 一會兒再看。

由于隊列的大小給的夠大,所以結(jié)合目前的情況來看應(yīng)當是線程池里的任務(wù)處理較慢,導(dǎo)致隊列的任務(wù)越堆越多,至少這是目前可以得出的結(jié)論。
線程分析

再來看看線程的分析,這里利用 fastthread.io 這個網(wǎng)站進行線程分析。

因為從表現(xiàn)來看線程池里的任務(wù)遲遲沒有執(zhí)行完畢,所以主要看看它們在干嘛。

正好他們都處于 RUNNABLE 狀態(tài),同時堆棧如下:

發(fā)現(xiàn)正好就是在處理上文提到的 HashSet,看這個堆棧是在查詢 key 是否存在。通過查看 312 行的業(yè)務(wù)代碼確實也是如此。

這里的線程名字也是個坑,讓我找了好久。
定位

分析了內(nèi)存和線程的堆棧之后其實已經(jīng)大概猜出一些問題了。

這里其實有一個前提忘記講到:

這個告警是凌晨三點發(fā)出的郵件,但并沒有電話提醒之類的,所以大家都不知道。

到了早上上班時才發(fā)現(xiàn)并立即 dump 了上面的證據(jù)。

所有有一個很重要的事實:這幾個業(yè)務(wù)線程在查詢 HashSet 的時候運行了 6 7 個小時都沒有返回

通過之前的監(jiān)控曲線圖也可以看出:

操作系統(tǒng)在之前一直處于高負載中,直到我們早上看到報警重啟之后才降低。

同時發(fā)現(xiàn)這個應(yīng)用生產(chǎn)上運行的是 JDK1.7 ,所以我初步認為應(yīng)該是在查詢 key 的時候進入了 HashMap 的環(huán)形鏈表導(dǎo)致 CPU 高負載同時也進入了死循環(huán)。

為了驗證這個問題再次 review 了代碼。

整理之后的偽代碼如下:

//線程池
private ExecutorService executor;

private Set set = new hashSet();

private void execute(){
    
    while(true){
        //從 MQ 中獲取數(shù)據(jù)
        String key = subMQ();
        executor.excute(new Worker(key)) ;
    }
}

public class Worker extends Thread{
    private String key ;

    public Worker(String key){
        this.key = key;
    }

    @Override
    private void run(){
        if(!set.contains(key)){

            //數(shù)據(jù)庫查詢
            if(queryDB(key)){
                set.add(key);
                return;
            }
        }

        //達到某種條件時清空 set
        if(flag){
            set = null ;
        }
    }    
}

大致的流程如下:

源源不斷的從 MQ 中獲取數(shù)據(jù)。

將數(shù)據(jù)丟到業(yè)務(wù)線程池中。

判斷數(shù)據(jù)是否已經(jīng)寫入了 Set。

沒有則查詢數(shù)據(jù)庫。

之后寫入到 Set 中。

這里有一個很明顯的問題,那就是作為共享資源的 Set 并沒有做任何的同步處理。

這里會有多個線程并發(fā)的操作,由于 HashSet 其實本質(zhì)上就是 HashMap,所以它肯定是線程不安全的,所以會出現(xiàn)兩個問題:

Set 中的數(shù)據(jù)在并發(fā)寫入時被覆蓋導(dǎo)致數(shù)據(jù)不準確。

會在擴容的時候形成環(huán)形鏈表。

第一個問題相對于第二個還能接受。

通過上文的內(nèi)存分析我們已經(jīng)知道這個 set 中的數(shù)據(jù)已經(jīng)不少了。同時由于初始化時并沒有指定大小,僅僅只是默認值,所以在大量的并發(fā)寫入時候會導(dǎo)致頻繁的擴容,而在 1.7 的條件下又可能會形成環(huán)形鏈表。

不巧的是代碼中也有查詢操作(contains()),觀察上文的堆棧情況:

發(fā)現(xiàn)是運行在 HashMap 的 465 行,來看看 1.7 中那里具體在做什么:

已經(jīng)很明顯了。這里在遍歷鏈表,同時由于形成了環(huán)形鏈表導(dǎo)致這個 e.next 永遠不為空,所以這個循環(huán)也不會退出了。

到這里其實已經(jīng)找到問題了,但還有一個疑問是為什么線程池里的任務(wù)隊列會越堆越多。我第一直覺是任務(wù)執(zhí)行太慢導(dǎo)致的。

仔細查看了代碼發(fā)現(xiàn)只有一個地方可能會慢:也就是有一個數(shù)據(jù)庫的查詢

把這個 SQL 拿到生產(chǎn)環(huán)境執(zhí)行發(fā)現(xiàn)確實不快,查看索引發(fā)現(xiàn)都有命中。

但我一看表中的數(shù)據(jù)發(fā)現(xiàn)已經(jīng)快有 7000W 的數(shù)據(jù)了。同時經(jīng)過運維得知 MySQL 那臺服務(wù)器的 IO 壓力也比較大。

所以這個原因也比較明顯了:

由于每消費一條數(shù)據(jù)都要去查詢一次數(shù)據(jù)庫,MySQL 本身壓力就比較大,加上數(shù)據(jù)量也很高所以導(dǎo)致這個 IO 響應(yīng)較慢,導(dǎo)致整個任務(wù)處理的就比較慢了。

但還有一個原因也不能忽視;由于所有的業(yè)務(wù)線程在某個時間點都進入了死循環(huán),根本沒有執(zhí)行完任務(wù)的機會,而后面的數(shù)據(jù)還在源源不斷的進入,所以這個隊列只會越堆越多!

這其實是一個老應(yīng)用了,可能會有人問為什么之前沒出現(xiàn)問題。

這是因為之前數(shù)據(jù)量都比較少,即使是并發(fā)寫入也沒有出現(xiàn)并發(fā)擴容形成環(huán)形鏈表的情況。這段時間業(yè)務(wù)量的暴增正好把這個隱藏的雷給揪出來了。所以還是得信墨菲他老人家的話。

總結(jié)

至此整個排查結(jié)束,而我們后續(xù)的調(diào)整措施大概如下:

HashSet 不是線程安全的,換為 ConcurrentHashMap同時把 value 寫死一樣可以達到 set 的效果。

根據(jù)我們后面的監(jiān)控,初始化 ConcurrentHashMap 的大小盡量大一些,避免頻繁的擴容。

MySQL 中很多數(shù)據(jù)都已經(jīng)不用了,進行冷熱處理。盡量降低單表數(shù)據(jù)量。同時后期考慮分表。

查數(shù)據(jù)那里調(diào)整為查緩存,提高查詢效率。

線程池的名稱一定得取的有意義,不然是自己給自己增加難度。

根據(jù)監(jiān)控將線程池的隊列大小調(diào)整為一個具體值,并且要有拒絕策略。

升級到 JDK1.8。

再一個是報警郵件酌情考慮為電話通知

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/72073.html

相關(guān)文章

  • Java多線程&高并發(fā)

    摘要:線程啟動規(guī)則對象的方法先行發(fā)生于此線程的每一個動作。所以局部變量是不被多個線程所共享的,也就不會出現(xiàn)并發(fā)問題。通過獲取到數(shù)據(jù),放入當前線程處理完之后將當前線程中的信息移除。主線程必須在啟動其他線程后立即調(diào)用方法。 一、線程安全性 定義:當多個線程訪問某個類時,不管運行時環(huán)境采用何種調(diào)度方式,或者這些線程將如何交替執(zhí)行,并且在主調(diào)代碼中不需要任何額外的同步或協(xié)同,這個類都能表現(xiàn)出正確的行...

    SQC 評論0 收藏0
  • Java編程中那些再熟悉不過知識點(持續(xù)更新)

    摘要:語言通過字節(jié)碼的方式,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題,同時又保留了解釋型語言可移植的特點。有針對不同系統(tǒng)的特定實現(xiàn),,,目的是使用相同的字節(jié)碼,它們都會給出相同的結(jié)果。項目主要基于捐贈的源代碼。 本文來自于我的慕課網(wǎng)手記:Java編程中那些再熟悉不過的知識點,轉(zhuǎn)載請保留鏈接 ;) 1. 面向?qū)ο蠛兔嫦蜻^程的區(qū)別 面向過程 優(yōu)點: 性能比面向?qū)ο蟾?。因為類調(diào)用時需要實例...

    taowen 評論0 收藏0
  • java中ConcurrentHashMap使用及在Java 8中沖突方案

    摘要:中的使用及在中的沖突方案引言簡稱是在作為的替代選擇新引入的,是包的重要成員。為了解決在頻繁沖突時性能降低的問題,中使用平衡樹來替代鏈表存儲沖突的元素。目前,只有和會在頻繁沖突的情況下使用平衡樹。 java中ConcurrentHashMap的使用及在Java 8中的沖突方案 1、引言 ConcurrentHashMap(簡稱CHM)是在Java 1.5作為Hashtable的替代選擇新...

    kun_jian 評論0 收藏0
  • java降低競爭鎖一些方法

    摘要:減少鎖的持有時間降低發(fā)生競爭可能性的一種有效方式就是盡可能縮短鎖的持有時間。代替獨占鎖第三種降低競爭鎖的影響的技術(shù)就是放棄使用獨占鎖,從而有助于使用一種友好并發(fā)的方式來管理共享狀態(tài)。 序 本文介紹一下提升并發(fā)可伸縮性的一些方式:減少鎖的持有時間,降低鎖的粒度,鎖分段、避免熱點域以及采用非獨占的鎖或非阻塞鎖來代替獨占鎖。 減少鎖的持有時間 降低發(fā)生競爭可能性的一種有效方式就是盡可能縮短鎖...

    novo 評論0 收藏0
  • 一次慘痛面試經(jīng)歷

    摘要:把內(nèi)存分成兩種,一種叫做棧內(nèi)存,一種叫做堆內(nèi)存在函數(shù)中定義的一些基本類型的變量和對象的引用變量都是在函數(shù)的棧內(nèi)存中分配。堆內(nèi)存用于存放由創(chuàng)建的對象和數(shù)組。 一次慘痛的阿里技術(shù)面 就在昨天,有幸接到了阿里的面試通知,本來我以為自己的簡歷應(yīng)該不會的到面試的機會了,然而機會卻這么來了,我卻沒有做好準備,被面試官大大一通血虐。因此,我想寫點東西紀念一下這次的經(jīng)歷,也當一次教訓(xùn)了。其實面試官大大...

    CoorChice 評論0 收藏0

發(fā)表評論

0條評論

fjcgreat

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<