久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

HashMap多線程并發(fā)問題分析

 YanYMLu 2016-05-24

并發(fā)問題的癥狀

多線程put后可能導(dǎo)致get死循環(huán)

從前我們的Java代碼因?yàn)橐恍┰蚴褂昧薍ashMap這個(gè)東西,,但是當(dāng)時(shí)的程序是單線程的,,一切都沒有問題,。后來,,我們的程序性能有問題,,所以需要變成多線程的,,于是,,變成多線程后到了線上,發(fā)現(xiàn)程序經(jīng)常占了100%的CPU,查看堆棧,,你會發(fā)現(xiàn)程序都Hang在了HashMap.get()這個(gè)方法上了,,重啟程序后問題消失。但是過段時(shí)間又會來,。而且,,這個(gè)問題在測試環(huán)境里可能很難重現(xiàn)。

我們簡單的看一下我們自己的代碼,,我們就知道HashMap被多個(gè)線程操作,。而Java的文檔說HashMap是非線程安全的,應(yīng)該用ConcurrentHashMap,。但是在這里我們可以來研究一下原因,。簡單代碼如下:

package com.king.hashmap; import java.util.HashMap; public class TestLock { private HashMap map = new HashMap(); public TestLock() { Thread t1 = new Thread() { public void run() { for (int i = 0; i < 50000;="" i++)="" {="" map.put(new="" integer(i),="" i);="" }="" system.out.println('t1="" over');="" }="" };="" thread="" t2="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.put(new="" integer(i),="" i);="" }="" system.out.println('t2="" over');="" }="" };="" thread="" t3="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.put(new="" integer(i),="" i);="" }="" system.out.println('t3="" over');="" }="" };="" thread="" t4="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.put(new="" integer(i),="" i);="" }="" system.out.println('t4="" over');="" }="" };="" thread="" t5="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.put(new="" integer(i),="" i);="" }="" system.out.println('t5="" over');="" }="" };="" thread="" t6="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.get(new="" integer(i));="" }="" system.out.println('t6="" over');="" }="" };="" thread="" t7="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.get(new="" integer(i));="" }="" system.out.println('t7="" over');="" }="" };="" thread="" t8="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.get(new="" integer(i));="" }="" system.out.println('t8="" over');="" }="" };="" thread="" t9="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.get(new="" integer(i));="" }="" system.out.println('t9="" over');="" }="" };="" thread="" t10="new" thread()="" {="" public="" void="" run()="" {="" for="" (int="" i="0;" i="">< 50000;="" i++)="" {="" map.get(new="" integer(i));="" }="" system.out.println('t10="" over');="" }="" };="" t1.start();="" t2.start();="" t3.start();="" t4.start();="" t5.start();="" t6.start();="" t7.start();="" t8.start();="" t9.start();="" t10.start();="" }="" public="" static="" void="" main(string[]="" args)="" {="" new="" testlock();="" }="" }="">

就是啟了10個(gè)線程,不斷的往一個(gè)非線程安全的HashMap中put內(nèi)容/get內(nèi)容,,put的內(nèi)容很簡單,key和value都是從0自增的整數(shù)(這個(gè)put的內(nèi)容做的并不好,,以致于后來干擾了我分析問題的思路),。對HashMap做并發(fā)寫操作,我原以為只不過會產(chǎn)生臟數(shù)據(jù)的情況,,但反復(fù)運(yùn)行這個(gè)程序,,會出現(xiàn)線程t1、t2被hang住的情況,,多數(shù)情況下是一個(gè)線程被hang住另一個(gè)成功結(jié)束,,偶爾會10個(gè)線程都被hang住。

產(chǎn)生這個(gè)死循環(huán)的根源在于對一個(gè)未保護(hù)的共享變量 — 一個(gè)'HashMap'數(shù)據(jù)結(jié)構(gòu)的操作,。當(dāng)在所有操作的方法上加了'synchronized'后,,一切恢復(fù)了正常。這算jvm的bug嗎,?應(yīng)該說不是的,,這個(gè)現(xiàn)象很早以前就報(bào)告出來了。Sun的工程師并不認(rèn)為這是bug,,而是建議在這樣的場景下應(yīng)采用'ConcurrentHashMap”,,

CPU利用率過高一般是因?yàn)槌霈F(xiàn)了出現(xiàn)了死循環(huán),導(dǎo)致部分線程一直運(yùn)行,,占用cpu時(shí)間。問題原因就是HashMap是非線程安全的,,多個(gè)線程put的時(shí)候造成了某個(gè)key值Entry key List的死循環(huán),,問題就這么產(chǎn)生了。

當(dāng)另外一個(gè)線程get 這個(gè)Entry List 死循環(huán)的key的時(shí)候,,這個(gè)get也會一直執(zhí)行,。最后結(jié)果是越來越多的線程死循環(huán),最后導(dǎo)致服務(wù)器dang掉,。我們一般認(rèn)為HashMap重復(fù)插入某個(gè)值的時(shí)候,,會覆蓋之前的值,這個(gè)沒錯(cuò),。但是對于多線程訪問的時(shí)候,,由于其內(nèi)部實(shí)現(xiàn)機(jī)制(在多線程環(huán)境且未作同步的情況下,對同一個(gè)HashMap做put操作可能導(dǎo)致兩個(gè)或以上線程同時(shí)做rehash動作,,就可能導(dǎo)致循環(huán)鍵表出現(xiàn),,一旦出現(xiàn)線程將無法終止,持續(xù)占用CPU,,導(dǎo)致CPU使用率居高不下),,就可能出現(xiàn)安全問題了,。

使用jstack工具dump出問題的那臺服務(wù)器的棧信息,。死循環(huán)的話,首先查找RUNNABLE的線程,,找到問題代碼如下:

java.lang.Thread.State:RUNNABLE
at java.util.HashMap.get(HashMap.java:303)
at com.sohu.twap.service.logic.TransformTweeter.doTransformTweetT5(TransformTweeter.java:183)
共出現(xiàn)了23次,。
java.lang.Thread.State:RUNNABLE
at java.util.HashMap.put(HashMap.java:374)
at com.sohu.twap.service.logic.TransformTweeter.transformT5(TransformTweeter.java:816)
共出現(xiàn)了3次,。

注意:不合理使用HashMap導(dǎo)致出現(xiàn)的是死循環(huán)而不是死鎖,。

多線程put的時(shí)候可能導(dǎo)致元素丟失

主要問題出在addEntry方法的new Entry (hash, key, value, e),如果兩個(gè)線程都同時(shí)取得了e,則他們下一個(gè)元素都是e,,然后賦值給table元素的時(shí)候有一個(gè)成功有一個(gè)丟失。

put非null元素后get出來的卻是null

在transfer方法中代碼如下:

void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; for (int j = 0; j < src.length;="" j++)="" {="" entry="" e="src[j];" if="" (e="" !="null)" {="" src[j]="null;" do="" {="" entry="" next="e.next;" int="" i="indexFor(e.hash," newcapacity);="" e.next="newTable[i];" newtable[i]="e;" e="next;" }="" while="" (e="" !="null);" }="" }="" }="">

在這個(gè)方法里,將舊數(shù)組賦值給src,,遍歷src,,當(dāng)src的元素非null時(shí),就將src中的該元素置null,,即將舊數(shù)組中的元素置null了,也就是這一句:

if (e != null) { src[j] = null;

此時(shí)若有g(shù)et方法訪問這個(gè)key,,它取得的還是舊數(shù)組,,當(dāng)然就取不到其對應(yīng)的value了。

總結(jié):HashMap未同步時(shí)在并發(fā)程序中會產(chǎn)生許多微妙的問題,難以從表層找到原因,。所以使用HashMap出現(xiàn)了違反直覺的現(xiàn)象,,那么可能就是并發(fā)導(dǎo)致的了。

HashMap數(shù)據(jù)結(jié)構(gòu)

我需要簡單地說一下HashMap這個(gè)經(jīng)典的數(shù)據(jù)結(jié)構(gòu),。

HashMap通常會用一個(gè)指針數(shù)組(假設(shè)為table[])來做分散所有的key,,當(dāng)一個(gè)key被加入時(shí),會通過Hash算法通過key算出這個(gè)數(shù)組的下標(biāo)i,,然后就把這個(gè) 插到table[i]中,,如果有兩個(gè)不同的key被算在了同一個(gè)i,那么就叫沖突,,又叫碰撞,,這樣會在table[i]上形成一個(gè)鏈表。

我們知道,,如果table[]的尺寸很小,,比如只有2個(gè),如果要放進(jìn)10個(gè)keys的話,,那么碰撞非常頻繁,,于是一個(gè)O(1)的查找算法,就變成了鏈表遍歷,,性能變成了O(n),,這是Hash表的缺陷。

所以,,Hash表的尺寸和容量非常的重要,。一般來說,Hash表這個(gè)容器當(dāng)有數(shù)據(jù)要插入時(shí),,都會檢查容量有沒有超過設(shè)定的thredhold,,如果超過,需要增大Hash表的尺寸,,但是這樣一來,,整個(gè)Hash表里的元素都需要被重算一遍。這叫rehash,,這個(gè)成本相當(dāng)?shù)拇蟆?/p>

HashMap的rehash源代碼

下面,,我們來看一下Java的HashMap的源代碼。Put一個(gè)Key,Value對到Hash表中:

public V put(K key, V value) { ...... //算Hash值 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //如果該key已被插入,,則替換掉舊的value (鏈接操作) for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //該key不存在,,需要增加一個(gè)結(jié)點(diǎn) addEntry(hash, key, value, i); return null; }

檢查容量是否超標(biāo):

void addEntry(int hash, K key, V value, int bucketIndex) { Entry e = table[bucketIndex]; table[bucketIndex] = new Entry(hash, key, value, e); //查看當(dāng)前的size是否超過了我們設(shè)定的閾值threshold,如果超過,,需要resize if (size++ >= threshold) resize(2 * table.length); }

新建一個(gè)更大尺寸的hash表,,然后把數(shù)據(jù)從老的Hash表中遷移到新的Hash表中,。

void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; ...... //創(chuàng)建一個(gè)新的Hash Table Entry[] newTable = new Entry[newCapacity]; //將Old Hash Table上的數(shù)據(jù)遷移到New Hash Table上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }

遷移的源代碼,注意高亮處:

void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; //下面這段代碼的意思是: // 從OldTable里摘一個(gè)元素出來,,然后放到NewTable中 for (int j = 0; j < src.length;="" j++)="" {=""> e = src[j]; if (e != null) { src[j] = null; do { Entry next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }

好了,,這個(gè)代碼算是比較正常的。而且沒有什么問題,。

正常的ReHash過程

畫了個(gè)圖做了個(gè)演示,。

  1. 我假設(shè)了我們的hash算法就是簡單的用key mod 一下表的大小(也就是數(shù)組的長度),。
  2. 最上面的是old hash 表,,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都沖突在table1這里了,。
  3. 接下來的三個(gè)步驟是Hash表 resize成4,,然后所有的 重新rehash的過程。

在此輸入圖片描述

并發(fā)的Rehash過程

(1)假設(shè)我們有兩個(gè)線程,。我用紅色和淺藍(lán)色標(biāo)注了一下,。我們再回頭看一下我們的 transfer代碼中的這個(gè)細(xì)節(jié):

do { Entry next = e.next; // <--假設(shè)線程一執(zhí)行到這里就被調(diào)度掛起了 int="" i="indexFor(e.hash," newcapacity);="" e.next="newTable[i];" newtable[i]="e;" e="next;" }="" while="" (e="" !="null);">

而我們的線程二執(zhí)行完成了。于是我們有下面的這個(gè)樣子,。
在此輸入圖片描述

注意:因?yàn)門hread1的 e 指向了key(3),,而next指向了key(7),其在線程二rehash后,,指向了線程二重組后的鏈表,。我們可以看到鏈表的順序被反轉(zhuǎn)后。
(2)線程一被調(diào)度回來執(zhí)行,。

  1. 先是執(zhí)行 newTalbe[i] = e,。
  2. 然后是e = next,,導(dǎo)致了e指向了key(7),。
  3. 而下一次循環(huán)的next = e.next導(dǎo)致了next指向了key(3)。

在此輸入圖片描述
(3)一切安好,。
線程一接著工作,。把key(7)摘下來,放到newTable[i]的第一個(gè),,然后把e和next往下移,。
在此輸入圖片描述
(4)環(huán)形鏈接出現(xiàn)。
e.next = newTable[i] 導(dǎo)致 key(3).next 指向了 key(7),。注意:此時(shí)的key(7).next 已經(jīng)指向了key(3),, 環(huán)形鏈表就這樣出現(xiàn)了。
在此輸入圖片描述
于是,,當(dāng)我們的線程一調(diào)用到,,HashTable.get(11)時(shí),,悲劇就出現(xiàn)了——Infinite Loop。

三種解決方案

Hashtable替換HashMap

Hashtable 是同步的,,但由迭代器返回的 Iterator 和由所有 Hashtable 的“collection 視圖方法”返回的 Collection 的 listIterator 方法都是快速失敗的:在創(chuàng)建 Iterator 之后,,如果從結(jié)構(gòu)上對 Hashtable 進(jìn)行修改,除非通過 Iterator 自身的移除或添加方法,,否則在任何時(shí)間以任何方式對其進(jìn)行修改,,Iterator 都將拋出 ConcurrentModificationException。因此,,面對并發(fā)的修改,,Iterator 很快就會完全失敗,而不冒在將來某個(gè)不確定的時(shí)間發(fā)生任意不確定行為的風(fēng)險(xiǎn),。由 Hashtable 的鍵和值方法返回的 Enumeration 不是快速失敗的,。

注意,迭代器的快速失敗行為無法得到保證,,因?yàn)橐话銇碚f,,不可能對是否出現(xiàn)不同步并發(fā)修改做出任何硬性保證??焖偈〉鲿M最大努力拋出 ConcurrentModificationException,。因此,為提高這類迭代器的正確性而編寫一個(gè)依賴于此異常的程序是錯(cuò)誤做法:迭代器的快速失敗行為應(yīng)該僅用于檢測程序錯(cuò)誤,。

Collections.synchronizedMap將HashMap包裝起來

返回由指定映射支持的同步(線程安全的)映射,。為了保證按順序訪問,必須通過返回的映射完成對底層映射的所有訪問,。在返回的映射或其任意 collection 視圖上進(jìn)行迭代時(shí),,強(qiáng)制用戶手工在返回的映射上進(jìn)行同步:

Map m = Collections.synchronizedMap(new HashMap()); ... Set s = m.keySet(); // Needn't be in synchronized block ... synchronized(m) { // Synchronizing on m, not s! Iterator i = s.iterator(); // Must be in synchronized block while (i.hasNext()) foo(i.next()); }

不遵從此建議將導(dǎo)致無法確定的行為。如果指定映射是可序列化的,,則返回的映射也將是可序列化的,。

ConcurrentHashMap替換HashMap

支持檢索的完全并發(fā)和更新的所期望可調(diào)整并發(fā)的哈希表。此類遵守與 Hashtable 相同的功能規(guī)范,,并且包括對應(yīng)于 Hashtable 的每個(gè)方法的方法版本,。不過,盡管所有操作都是線程安全的,,但檢索操作不必鎖定,,并且不支持以某種防止所有訪問的方式鎖定整個(gè)表。此類可以通過程序完全與 Hashtable 進(jìn)行互操作,,這取決于其線程安全,,而與其同步細(xì)節(jié)無關(guān)。
檢索操作(包括 get)通常不會受阻塞,,因此,,可能與更新操作交迭(包括 put 和 remove),。檢索會影響最近完成的更新操作的結(jié)果。對于一些聚合操作,,比如 putAll 和 clear,,并發(fā)檢索可能只影響某些條目的插入和移除。類似地,,在創(chuàng)建迭代器/枚舉時(shí)或自此之后,,Iterators 和 Enumerations 返回在某一時(shí)間點(diǎn)上影響哈希表狀態(tài)的元素。它們不會拋出 ConcurrentModificationException,。不過,,迭代器被設(shè)計(jì)成每次僅由一個(gè)線程使用。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多