最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一次因HashSet引起的并發(fā)問題詳解

 更新時(shí)間:2018年11月08日 10:04:13   作者:crossoverjie  
這篇文章主要給大家介紹了一次因HashSet引起的并發(fā)問題的解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧

為啥要用HahSet?

假如我們現(xiàn)在想要在一大堆數(shù)據(jù)中查找X數(shù)據(jù)。LinkedList的數(shù)據(jù)結(jié)構(gòu)就不說(shuō)了,查找效率低的可怕。ArrayList哪,如果我們不知道X的位置序號(hào),還是一樣要全部遍歷一次直到查到結(jié)果,效率一樣可怕。HashSet天生就是為了提高查找效率的。

背景

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

心中暗道不好,因?yàn)楸O(jiān)控系統(tǒng)從來(lái)不會(huì)告訴我應(yīng)用完美無(wú) bug,其實(shí)系統(tǒng)挺猥瑣。

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

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

分析

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

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

而報(bào)警的隊(duì)列正好就是這個(gè)線程池的隊(duì)列。

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

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

采用的是默認(rèn)的 LinkedBlockingQueue 并沒有指定大?。ㄟ@也是個(gè)坑),于是這個(gè)隊(duì)列的默認(rèn)大小為 Integer.MAX_VALUE。

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

內(nèi)存分析

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

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

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

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

線程分析

再來(lái)看看線程的分析,這里利用fastthread.io 這個(gè)網(wǎng)站進(jìn)行線程分析。

因?yàn)閺谋憩F(xiàn)來(lái)看線程池里的任務(wù)遲遲沒有執(zhí)行完畢,所以主要看看它們?cè)诟陕铩?/p>

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

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

這里的線程名字也是個(gè)坑,讓我找了好久。

定位

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

這里其實(shí)有一個(gè)前提忘記講到:

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

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

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

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

操作系統(tǒng)在之前一直處于高負(fù)載中,直到我們?cè)缟峡吹綀?bào)警重啟之后才降低。

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

為了驗(yàn)證這個(gè)問題再次 review 了代碼。

整理之后的偽代碼如下:

//線程池
private ExecutorService executor;
private Set<String> 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ù)庫(kù)查詢
 if(queryDB(key)){
 set.add(key);
 return;
 }
 }
 //達(dá)到某種條件時(shí)清空 set
 if(flag){
 set = null ;
 }
 } 
}

大致的流程如下:

  • 源源不斷的從 MQ 中獲取數(shù)據(jù)。
  • 將數(shù)據(jù)丟到業(yè)務(wù)線程池中。
  • 判斷數(shù)據(jù)是否已經(jīng)寫入了 Set。
  • 沒有則查詢數(shù)據(jù)庫(kù)。
  • 之后寫入到 Set 中。

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

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

  • Set 中的數(shù)據(jù)在并發(fā)寫入時(shí)被覆蓋導(dǎo)致數(shù)據(jù)不準(zhǔn)確。
  • 會(huì)在擴(kuò)容的時(shí)候形成環(huán)形鏈表。

第一個(gè)問題相對(duì)于第二個(gè)還能接受。

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

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

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

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

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

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

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

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

所以這個(gè)原因也比較明顯了:

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

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

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

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

總結(jié)

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

  • HashSet 不是線程安全的,換為 ConcurrentHashMap同時(shí)把 value 寫死一樣可以達(dá)到 set 的效果。
  • 根據(jù)我們后面的監(jiān)控,初始化 ConcurrentHashMap 的大小盡量大一些,避免頻繁的擴(kuò)容。
  • MySQL 中很多數(shù)據(jù)都已經(jīng)不用了,進(jìn)行冷熱處理。盡量降低單表數(shù)據(jù)量。同時(shí)后期考慮分表。
  • 查數(shù)據(jù)那里調(diào)整為查緩存,提高查詢效率。
  • 線程池的名稱一定得取的有意義,不然是自己給自己增加難度。
  • 根據(jù)監(jiān)控將線程池的隊(duì)列大小調(diào)整為一個(gè)具體值,并且要有拒絕策略。
  • 升級(jí)到 JDK1.8。
  • 再一個(gè)是報(bào)警郵件酌情考慮為電話通知😂。

HashMap 的死循環(huán)問題在網(wǎng)上層出不窮,沒想到還真被我遇到了。現(xiàn)在要滿足這個(gè)條件還是挺少見的,比如 1.8 以下的 JDK 這一條可能大多數(shù)人就碰不到,正好又證實(shí)了一次墨菲定律。

好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。

相關(guān)文章

  • hadoop序列化實(shí)現(xiàn)案例代碼

    hadoop序列化實(shí)現(xiàn)案例代碼

    序列化想必大家都很熟悉了,對(duì)象在進(jìn)行網(wǎng)絡(luò)傳輸過程中,需要序列化之后才能傳輸?shù)娇蛻舳?或者客戶端的數(shù)據(jù)序列化之后送達(dá)到服務(wù)端,本文將為大家介紹Hadoop如何實(shí)現(xiàn)序列化,需要的可以參考一下
    2022-01-01
  • 基于LinkedHashMap實(shí)現(xiàn)LRU緩存

    基于LinkedHashMap實(shí)現(xiàn)LRU緩存

    LinkedHashMap是Java集合中一個(gè)常用的容器,它繼承了HashMap, 是一個(gè)有序的Hash表。那么該如何基于LinkedHashMap實(shí)現(xiàn)一個(gè)LRU緩存呢?本文將介紹LinkedHashMap的實(shí)現(xiàn)原理,感興趣的同學(xué)可以參考一下
    2023-05-05
  • JAVA多線程和并發(fā)基礎(chǔ)面試問答(翻譯)

    JAVA多線程和并發(fā)基礎(chǔ)面試問答(翻譯)

    多線程和并發(fā)問題是Java技術(shù)面試中面試官比較喜歡問的問題之一。在這里,從面試的角度列出了大部分重要的問題,但是你仍然應(yīng)該牢固的掌握J(rèn)ava多線程基礎(chǔ)知識(shí)來(lái)對(duì)應(yīng)日后碰到的問題
    2014-09-09
  • java中xml進(jìn)行報(bào)文發(fā)送和解析操作

    java中xml進(jìn)行報(bào)文發(fā)送和解析操作

    這篇文章主要介紹了java中xml進(jìn)行報(bào)文發(fā)送和解析操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧
    2020-10-10
  • 淺談Spring Boot日志框架實(shí)踐

    淺談Spring Boot日志框架實(shí)踐

    這篇文章主要介紹了淺談Spring Boot日志框架實(shí)踐,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧
    2018-03-03
  • 詳解Spring Boot 定制HTTP消息轉(zhuǎn)換器

    詳解Spring Boot 定制HTTP消息轉(zhuǎn)換器

    本篇文章主要介紹了詳解Spring Boot 定制HTTP消息轉(zhuǎn)換器,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧
    2017-11-11
  • Spring?Boot配置文件的語(yǔ)法規(guī)則詳解(properties和yml)

    Spring?Boot配置文件的語(yǔ)法規(guī)則詳解(properties和yml)

    這篇文章主要介紹了Spring?Boot配置文件的語(yǔ)法規(guī)則,主要介紹兩種配置文件的語(yǔ)法和格式,properties和yml,對(duì)于配置文件也有獨(dú)立的文件夾存放,主要用來(lái)存放一些需要經(jīng)過變動(dòng)的數(shù)據(jù)(變量值),感興趣的朋友跟隨小編一起看看吧
    2024-07-07
  • Java 如何實(shí)現(xiàn)一個(gè)http服務(wù)器

    Java 如何實(shí)現(xiàn)一個(gè)http服務(wù)器

    這篇文章主要介紹了Java 如何實(shí)現(xiàn)一個(gè)http服務(wù)器,幫助大家更好的理解和學(xué)習(xí)Java,感興趣的朋友可以了解下
    2020-11-11
  • SpringBoot根據(jù)參數(shù)動(dòng)態(tài)調(diào)用接口實(shí)現(xiàn)類方法

    SpringBoot根據(jù)參數(shù)動(dòng)態(tài)調(diào)用接口實(shí)現(xiàn)類方法

    在?Spring?Boot?開發(fā)中,我們經(jīng)常會(huì)遇到根據(jù)不同參數(shù)調(diào)用接口不同實(shí)現(xiàn)類方法的需求,本文將詳細(xì)介紹如何實(shí)現(xiàn)這一功能,有需要的小伙伴可以參考下
    2025-02-02
  • Spring+Junit4進(jìn)行接口測(cè)試實(shí)例代碼

    Spring+Junit4進(jìn)行接口測(cè)試實(shí)例代碼

    這篇文章主要介紹了Spring+Junit4進(jìn)行接口測(cè)試實(shí)例代碼,涉及pom.xml、bean的配置,以及接口測(cè)試代碼等相關(guān)內(nèi)容,小編覺得還是挺不錯(cuò)的,這里分享給大家,需要的朋友可以參考下
    2018-01-01

最新評(píng)論

泉州市| 钦州市| 石棉县| 旺苍县| 日照市| 土默特左旗| 丹巴县| 贵州省| 天全县| 奈曼旗| 东源县| 旬邑县| 邹平县| 突泉县| 温州市| 凤庆县| 银川市| 漳平市| 秀山| 扎兰屯市| 江津市| 尉犁县| 泗洪县| 镇赉县| 连山| 金寨县| 和田市| 临沧市| 崇州市| 于田县| 错那县| 两当县| 普兰县| 永安市| 吴川市| 当阳市| 民乐县| 宁强县| 栖霞市| 西林县| 根河市|