国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

一次 HashSet 所引起的并發問題

fjcgreat / 3500人閱讀

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

背景

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

心中暗道不好,因為監控系統從來不會告訴我應用完美無 bug,其實系統挺猥瑣。

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

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

分析

首先了解下這個應用大概是做什么的。

簡單來說就是從 MQ 中取出數據然后丟到后面的業務線程池中做具體的業務處理。

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

跟蹤代碼發現構建線程池的方式如下:

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

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

由于應用已經重啟,只能從僅存的線程快照和內存快照進行分析。

內存分析

先利用 MAT 分析了內存,的到了如下報告。

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

當然其中隊列占用了大量的內存,所以優先查看,HashSet 一會兒再看。

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

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

因為從表現來看線程池里的任務遲遲沒有執行完畢,所以主要看看它們在干嘛。

正好他們都處于 RUNNABLE 狀態,同時堆棧如下:

發現正好就是在處理上文提到的 HashSet,看這個堆棧是在查詢 key 是否存在。通過查看 312 行的業務代碼確實也是如此。

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

分析了內存和線程的堆棧之后其實已經大概猜出一些問題了。

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

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

到了早上上班時才發現并立即 dump 了上面的證據。

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

通過之前的監控曲線圖也可以看出:

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

同時發現這個應用生產上運行的是 JDK1.7 ,所以我初步認為應該是在查詢 key 的時候進入了 HashMap 的環形鏈表導致 CPU 高負載同時也進入了死循環。

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

整理之后的偽代碼如下:

//線程池
private ExecutorService executor;

private Set set = new hashSet();

private void execute(){
    
    while(true){
        //從 MQ 中獲取數據
        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)){

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

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

大致的流程如下:

源源不斷的從 MQ 中獲取數據。

將數據丟到業務線程池中。

判斷數據是否已經寫入了 Set。

沒有則查詢數據庫。

之后寫入到 Set 中。

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

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

Set 中的數據在并發寫入時被覆蓋導致數據不準確。

會在擴容的時候形成環形鏈表。

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

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

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

發現是運行在 HashMap 的 465 行,來看看 1.7 中那里具體在做什么:

已經很明顯了。這里在遍歷鏈表,同時由于形成了環形鏈表導致這個 e.next 永遠不為空,所以這個循環也不會退出了。

到這里其實已經找到問題了,但還有一個疑問是為什么線程池里的任務隊列會越堆越多。我第一直覺是任務執行太慢導致的。

仔細查看了代碼發現只有一個地方可能會慢:也就是有一個數據庫的查詢。

把這個 SQL 拿到生產環境執行發現確實不快,查看索引發現都有命中。

但我一看表中的數據發現已經快有 7000W 的數據了。同時經過運維得知 MySQL 那臺服務器的 IO 壓力也比較大。

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

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

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

這其實是一個老應用了,可能會有人問為什么之前沒出現問題。

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

總結

至此整個排查結束,而我們后續的調整措施大概如下:

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

根據我們后面的監控,初始化 ConcurrentHashMap 的大小盡量大一些,避免頻繁的擴容。

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

查數據那里調整為查緩存,提高查詢效率。

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

根據監控將線程池的隊列大小調整為一個具體值,并且要有拒絕策略。

升級到 JDK1.8。

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

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72073.html

相關文章

  • Java多線程&高并發

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

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

    摘要:語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。有針對不同系統的特定實現,,,目的是使用相同的字節碼,它們都會給出相同的結果。項目主要基于捐贈的源代碼。 本文來自于我的慕課網手記:Java編程中那些再熟悉不過的知識點,轉載請保留鏈接 ;) 1. 面向對象和面向過程的區別 面向過程 優點: 性能比面向對象高。因為類調用時需要實例...

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

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

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

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

    novo 評論0 收藏0
  • 一次慘痛面試經歷

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

    CoorChice 評論0 收藏0

發表評論

0條評論

fjcgreat

|高級講師

TA的文章

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