欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

為啥要用HahSet?

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

背景

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

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

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

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

分析

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

簡單來說就是從 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è)比較大的對象,一個(gè)就是之前線程池存放任務(wù)的 LinkedBlockingQueue,還有一個(gè)則是 HashSet。

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

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

線程分析

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

因?yàn)閺谋憩F(xiàn)來看線程池里的任務(wù)遲遲沒有執(zhí)行完畢,所以主要看看它們在干嘛。

正好他們都處于 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ù)載中,直到我們早上看到報(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ù)庫查詢
 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ù)庫。
  • 之后寫入到 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è)問題相對于第二個(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 行,來看看 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ù)庫的查詢。

把這個(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ù)庫,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è)隱藏的雷給揪出來了。所以還是得信墨菲他老人家的話。

總結(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è)具體值,并且要有拒絕策略。
  • 升級到 JDK1.8。
  • 再一個(gè)是報(bào)警郵件酌情考慮為電話通知😂。

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

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

相關(guān)文章

  • java實(shí)現(xiàn)代碼統(tǒng)計(jì)小程序

    java實(shí)現(xiàn)代碼統(tǒng)計(jì)小程序

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)代碼統(tǒng)計(jì)小程序,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-09-09
  • 實(shí)戰(zhàn)SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證

    實(shí)戰(zhàn)SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證

    本文詳細(xì)講解了SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-12-12
  • Springboot整合mqtt服務(wù)的示例代碼

    Springboot整合mqtt服務(wù)的示例代碼

    MQTT是一個(gè)基于客戶端-服務(wù)器的消息發(fā)布/訂閱傳輸協(xié)議。MQTT協(xié)議是輕量、簡單、開放和易于實(shí)現(xiàn)的,這些特點(diǎn)使它適用范圍非常廣泛。本文為大家分享了Springboot整合mqtt服務(wù)的示例代碼,需要的可以參考一下
    2022-03-03
  • Java文件讀寫IO/NIO及性能比較詳細(xì)代碼及總結(jié)

    Java文件讀寫IO/NIO及性能比較詳細(xì)代碼及總結(jié)

    這篇文章主要介紹了Java文件讀寫IO/NIO及性能比較詳細(xì)代碼及總結(jié),具有一定借鑒價(jià)值,需要的朋友可以參考下。
    2017-12-12
  • SpringBoot實(shí)現(xiàn)HTTP服務(wù)監(jiān)聽的代碼示例

    SpringBoot實(shí)現(xiàn)HTTP服務(wù)監(jiān)聽的代碼示例

    前后端分離項(xiàng)目中,在調(diào)用接口調(diào)試時(shí)候,我們可以通過cpolar內(nèi)網(wǎng)穿透將本地服務(wù)端接口模擬公共網(wǎng)絡(luò)環(huán)境遠(yuǎn)程調(diào)用調(diào)試,本次教程我們以Java服務(wù)端接口為例,需要的朋友可以參考下
    2023-05-05
  • Java的動(dòng)態(tài)綁定與雙分派_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    Java的動(dòng)態(tài)綁定與雙分派_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    這篇文章主要介紹了Java的動(dòng)態(tài)綁定與雙分派,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-08-08
  • Spring中的@Value和@PropertySource注解詳解

    Spring中的@Value和@PropertySource注解詳解

    這篇文章主要介紹了Spring中的@Value和@PropertySource注解詳解,@PropertySource:讀取外部配置文件中的key-value保存到運(yùn)行的環(huán)境變量中,本文提供了部分實(shí)現(xiàn)代碼,需要的朋友可以參考下
    2023-11-11
  • idea下載svn的項(xiàng)目并且運(yùn)行操作

    idea下載svn的項(xiàng)目并且運(yùn)行操作

    這篇文章主要介紹了idea下載svn的項(xiàng)目并且運(yùn)行操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-09-09
  • SpringBoot邏輯異常統(tǒng)一處理方法

    SpringBoot邏輯異常統(tǒng)一處理方法

    這篇文章主要介紹了SpringBoot邏輯異常統(tǒng)一處理方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-10-10
  • SpringBoot服務(wù)設(shè)置禁止server.point端口的使用

    SpringBoot服務(wù)設(shè)置禁止server.point端口的使用

    本文主要介紹了SpringBoot服務(wù)設(shè)置禁止server.point端口的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-01-01

最新評論