Java并發(fā)編程:HashMap與ConcurrentHashMap的線程安全性

版權聲明:本文為小斑馬學習總結文章饭望,技術來源于韋東山著作,轉載請注明出處!
最近無意中發(fā)現(xiàn)有很多對Map尤其是HashMap的線程安全性的話題討論打肝,在我的理解中,對HashMap的理解中也就知道它是線程不安全的挪捕,以及HashMap的底層算法采用了鏈地址法來解決哈希沖突的知識粗梭,但是對其線程安全性的認知有限,故寫這篇博客的目的就是讓和我一樣對這塊內容不熟悉的小伙伴有一個對级零。

一断医、哈希表

這里先說一下哈希表的定義:哈希表是一種根據(jù)關鍵碼去尋找值的數(shù)據(jù)映射結構,該結構通過把關鍵碼映射的位置去尋找存放值的地方奏纪,說起來可能感覺有點復雜鉴嗤,我想我舉個例子你就會明白了,最典型的的例子就是字典序调,大家估計小學的時候也用過不少新華字典吧醉锅,如果我想要獲取“按”字詳細信息,我肯定會去根據(jù)拼音an去查找 拼音索引(當然也可以是偏旁索引)发绢,我們首先去查an在字典的位置硬耍,查了一下得到“安”,結果如下朴摊。這過程就是鍵碼映射默垄,在公式里面,就是通過key去查找f(key)甚纲。其中口锭,按就是關鍵字(key),f()就是字典索引,也就是哈希函數(shù)鹃操,查到的頁碼4就是哈希值韭寸。

二、哈希沖突

但是問題又來了荆隘,我們要查的是“按”恩伺,而不是“安,但是他們的拼音都是一樣的椰拒。也就是通過關鍵字按和關鍵字安可以映射到一樣的字典頁碼4的位置晶渠,這就是哈希沖突(也叫哈希碰撞),在公式上表達就是key1≠key2燃观,但f(key1)=f(key2)褒脯。沖突會給查找?guī)砺闊阆胂肜禄伲惚緛聿檎业氖恰鞍础狈ǎ菂s找到“安”字,你又得向后翻一兩頁脊框,在計算機里面也是一樣道理的颁督。
但哈希沖突是無可避免的,為什么這么說呢浇雹,因為你如果要完全避開這種情況沉御,你只能每個字典去新開一個頁,然后每個字在索引里面都有對應的頁碼箫爷,這就可以避免沖突嚷节。但是會導致空間增大(每個字都有一頁)。
既然無法避免虎锚,就只能盡量減少沖突帶來的損失硫痰,而一個好的哈希函數(shù)需要有以下特點:

  • 1.盡量使關鍵字對應的記錄均勻分配在哈希表里面(比如說某廠商賣30棟房子,均勻劃分ABC3個區(qū)域窜护,如果你劃分A區(qū)域1個房子效斑,B區(qū)域1個房子,C區(qū)域28個房子柱徙,有人來查找C區(qū)域的某個房子最壞的情況就是要找28次)缓屠。

  • 2.關鍵字極小的變化可以引起哈希值極大的變化。

比較好的哈希函數(shù)是time33算法护侮。PHP的數(shù)組就是把這個作為哈希函數(shù)敌完。
核心的算法就是如下:

unsigned long hash(const char* key){
unsigned long hash=0;
for(int i=0;i<strlen(key);i++){
    hash = hash*33+str[i];
}  
return hash;
}
三、哈希沖突解決辦法

如果遇到?jīng)_突羊初,哈希表一般是怎么解決的呢滨溉?具體方法有很多什湘,百度也會有一堆,最常用的就是開發(fā)定址法和鏈地址法晦攒。
1.開發(fā)定址法
如果遇到?jīng)_突的時候怎么辦呢闽撤?就找hash表剩下空余的空間,找到空余的空間然后插入脯颜。就像你去商店買東西哟旗,發(fā)現(xiàn)東西賣光了,怎么辦呢栋操?找下一家有東西賣的商家買唄闸餐。

由于我沒有深入試驗過,所以貼上在書上的解釋:


2.鏈地址法
上面所說的開發(fā)定址法的原理是遇到?jīng)_突的時候查找順著原來哈希地址查找下一個空閑地址然后插入矾芙,但是也有一個問題就是如果空間不足绎巨,那他無法處理沖突也無法插入數(shù)據(jù),因此需要裝填因子(空間/插入數(shù)據(jù))>=1蠕啄。
那有沒有一種方法可以解決這種問題呢?鏈地址法可以戈锻,鏈地址法的原理時如果遇到?jīng)_突歼跟,他就會在原地址新建一個空間,然后以鏈表結點的形式插入到該空間格遭。我感覺業(yè)界上用的最多的就是鏈地址法哈街。下面從百度上截取來一張圖片,可以很清晰明了反應下面的結構拒迅。比如說我有一堆數(shù)據(jù){1,12,26,337,353...}骚秦,而我的哈希算法是H(key)=key mod 16,第一個數(shù)據(jù)1的哈希值f(1)=1璧微,插入到1結點的后面作箍,第二個數(shù)據(jù)12的哈希值f(12)=12,插入到12結點前硫,第三個數(shù)據(jù)26的哈希值f(26)=10胞得,插入到10結點后面,第4個數(shù)據(jù)337屹电,計算得到哈希值是1阶剑,遇到?jīng)_突,但是依然只需要找到該1結點的最后鏈結點插入即可危号,同理353牧愁。

四、HashMap底層實現(xiàn)

HashMap允許使用null作為key或者value外莲,并且HashMap不是線程安全的猪半,除了這兩點外,HashMap與Hashtable大致相同,下面是官方API對HashMap的描述:
Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.
如果有多個線程對Hash映射進行訪問办龄,那么至少有一個線程會對哈希映射進行結構的修改:結構上的修改是指添加或刪除一個或多個映射關系的任何操作烘绽;僅改變與實例已經(jīng)包含的鍵關聯(lián)的值不是結構上的修改。
那么很顯然俐填,當多個線程同時(嚴格來說不能稱為同時安接,因為CPU每次只能允許一個線程獲取資源,只不過時間上非常短英融,CPU運行速度很快盏檐,所以理解為同時)修改哈希映射,那么最終的哈希映射(就是哈希表)的最終結果是不能確定的驶悟,這只能看CPU心情了胡野。如果要解決這個問題,官方的參考方案是保持外部同步痕鳍,什么意思硫豆?看下面的代碼就知道了:

Map m = Collections.synchronizedMap(new HashMap(...));

但是不建議這么使用,因為當多個并發(fā)的非同步操作修改哈希表的時候笼呆,最終結果不可預測熊响,所以使用上面的方法創(chuàng)建HashMap的時候,當有多個線程并發(fā)訪問哈希表的情況下诗赌,會拋出異常汗茄,所以并發(fā)修改會失敗。比如下面這段代碼:

for (int i = 0; i < 20; i++) {
    collectionSynMap.put(i, String.valueOf(i));
}
Set<Entry<Integer,String>> keySets = collectionSynMap.entrySet();
Iterator<Entry<Integer, String>> keySetsIterator = keySets.iterator();
try {
    while(keySetsIterator.hasNext()){
        Entry<Integer,String> entrys = (Entry<Integer, String>) keySetsIterator.next();
        System.out.println(entrys.getValue());
        if(entrys.getValue().equals("1")){
            System.out.println(entrys.getValue());
            collectionSynMap.remove(1);
            //keySetsIterator.remove();
        }
    }
} catch (Exception e) {
    e.printStackTrace();

就會拋出ConcurrentModificationException異常铭若,因為在使用迭代器遍歷的時候修改映射結構洪碳,但是使用代碼中注釋的刪除是不會拋出異常的。
通過上面的分析叼屠,我們初步了解HashMap的非線程安全的原理瞳腌,下面從源碼的角度分析一下,為什么HashMap不是線程安全的:

public V put(K key, V value) {
   //這里省略了對重復鍵值的處理代碼
   modCount++;
   addEntry(hash, key, value, i);
   return null;
}

那么問題應該處在addEntry()上环鲤,下面來看看其源碼:

void addEntry(int hash, K key, V value, int bucketIndex) {
//如果達到Map的閾值纯趋,那么就擴大哈希表的容量
if ((size >= threshold) && (null != table[bucketIndex])) {
    //擴容
    resize(2 * table.length);
    hash = (null != key) ? hash(key) : 0;
    bucketIndex = indexFor(hash, table.length);
}
//創(chuàng)建Entry鍵值對,此處省略這部分代碼

假設有線程A和線程B都調用addEntry()方法冷离,線程A和B會得到當前哈希表位置的頭結點(就是上面鏈地址法的第一個元素)吵冒,并修改該位置的頭結點,如果是線程A先獲取頭結點西剥,那么B的操作就會覆蓋線程A的操作痹栖,所以會有問題。
下面再看看resize方法的源碼:

void resize(int newCapacity) {
//此處省略如果達到閾值擴容為原來兩倍的過程代碼
Entry[] newTable = new Entry[newCapacity];
//把當前的哈希表轉移到新的擴容后的哈希表中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

所以如果有多個線程執(zhí)行put方法瞭空,并調用resize方法揪阿,那么就會出現(xiàn)多種情況疗我,在轉移的過程中丟失數(shù)據(jù),或者擴容失敗南捂,都有可能吴裤,所以從源碼的角度分析這也是線程不安全的。
HashMap測試代碼

for (int i = 0; i < 40; i++) {
    hashMap.put(i, String.valueOf(i));
}
Set<Entry<Integer,String>> keySets = hashMap.entrySet();
final Iterator<Entry<Integer, String>> keySetsIterator = keySets.iterator();
Thread t3 = new Thread(){
public void run(){
    try {
        while(keySetsIterator.hasNext()){
            Entry<Integer,String> entrys = (Entry<Integer, String>) keySetsIterator.next();
            System.out.println(entrys.getValue());
            if(entrys.getValue().equals("1")){
                System.out.println(entrys.getValue());
                hashMap.remove(1);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
};
Thread t4 = new Thread(){
public void run(){
    try {
        while(keySetsIterator.hasNext()){
            Entry<Integer,String> entrys = (Entry<Integer, String>) keySetsIterator.next();
            System.out.println(entrys.getValue());
            if(entrys.getValue().equals("1")){
                System.out.println(entrys.getValue());
                hashMap.remove(1);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
};
t3.start();
t4.start();

這段代碼啟動了兩個線程并發(fā)修改HashMap的映射關系溺健,所以會拋出兩個ConcurrentModificationException異常麦牺,通過這段測試代碼在此證明了HashMap的非線程安全。
Hashtable的底層實現(xiàn)
在介紹HashMap提到Hashtable是線程安全的鞭缭,那么H啊時table是如何實現(xiàn)線程安全的呢剖膳?有了上面的介紹,我們直接從源碼中分析其線程安全性:

public synchronized V put(K key, V value) {
// 保證value值不為空岭辣,此處省略其代碼
// 保證key是不重復的吱晒,此處省略其代碼
//查過閾值則擴容,此處省略
// Creates the new entry.
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
return null;

通過源碼可以很明顯看到其put方法使用synchronized關鍵字沦童,在線程中這是實現(xiàn)線程安全的一種方式仑濒,所以Hashtable是線程安全的。
Hashtable的測試案例
下面使用一段測試代碼驗證Hashtable的線程安全:

Thread t3 = new Thread(){
    public void run(){
        for (int i = 0; i < 20; i++) {
            hashTable.put(i, String.valueOf(i));
        }
    }
};
Thread t4 = new Thread(){
    public void run(){
        for (int i = 20; i < 40; i++) {
            hashTable.put(i, String.valueOf(i));
        }
    }
};
t3.start();
t4.start();
//放完數(shù)據(jù)后偷遗,從map中取出數(shù)據(jù)躏精,如果map是線程安全的,那么取出的entry應該和放進去的一一對應
for (int i = 0; i < 40; i++) {
    System.out.println(i + "=" + hashTable.get(i));

最后得到的輸出結果是這樣的:
CocurrentHashMap詳解
ConcurrentHashMap的測試案例
下面仍然通過一段測試程序驗證ConcurrentHashMap的線程安全:

Thread t5 = new Thread(){
    public void run(){
        for (int i = 0; i < 20; i++) {
            concurrentHashMap.put(i, String.valueOf(i));
        }
    }
};
Thread t6 = new Thread(){
    public void run(){
        for (int i = 20; i < 40; i++) {
            concurrentHashMap.put(i, String.valueOf(i));
        }
    }
};
t5.start();
t6.start();
for (int i = 0; i < 40; i++) {
    System.out.println(i + "=" + concurrentHashMap.get(i));

最后鹦肿,控制臺輸出的結果如下:
說了那么多,針對Map子類的安全性可以總結如下幾點:

  • HashMap采用鏈地址法解決哈希沖突辅柴,多線程訪問哈希表的位置并修改映射關系的時候箩溃,后執(zhí)行的線程會覆蓋先執(zhí)行線程的修改,所以不是線程安全的
  • Hashtable采用synchronized關鍵字解決了并發(fā)訪問的安全性問題但是效率較低.碌嘀。
  • ConcurrentHashMap使用了線程鎖分段技術涣旨,每次訪問只允許一個線程修改哈希表的映射關系,所以是線程安全的
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末股冗,一起剝皮案震驚了整個濱河市霹陡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌止状,老刑警劉巖烹棉,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怯疤,居然都是意外死亡浆洗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門集峦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伏社,“玉大人抠刺,你說我怎么就攤上這事≌” “怎么了速妖?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長聪黎。 經(jīng)常有香客問我罕容,道長,這世上最難降的妖魔是什么挺举? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任杀赢,我火速辦了婚禮,結果婚禮上湘纵,老公的妹妹穿的比我還像新娘脂崔。我一直安慰自己,他們只是感情好梧喷,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布砌左。 她就那樣靜靜地躺著,像睡著了一般铺敌。 火紅的嫁衣襯著肌膚如雪汇歹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天偿凭,我揣著相機與錄音产弹,去河邊找鬼。 笑死弯囊,一個胖子當著我的面吹牛痰哨,可吹牛的內容都是我干的。 我是一名探鬼主播匾嘱,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼斤斧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霎烙?” 一聲冷哼從身側響起撬讽,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎悬垃,沒想到半個月后游昼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡尝蠕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年酱床,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片趟佃。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡扇谣,死狀恐怖昧捷,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情罐寨,我是刑警寧澤靡挥,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站鸯绿,受9級特大地震影響跋破,放射性物質發(fā)生泄漏。R本人自食惡果不足惜瓶蝴,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一毒返、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舷手,春花似錦拧簸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至歉眷,卻和暖如春牺六,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背汗捡。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工淑际, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人扇住。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓庸追,卻偏偏與公主長得像,于是被迫代替她去往敵國和親台囱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

推薦閱讀更多精彩內容

  • 本文是我自己在秋招復習時的讀書筆記读整,整理的知識點簿训,也是為了防止忘記,尊重勞動成果米间,轉載注明出處哦强品!如果你也喜歡,那...
    波波波先森閱讀 11,245評論 4 56
  • 在一個方法內部定義的變量都存儲在棧中屈糊,當這個函數(shù)運行結束后的榛,其對應的棧就會被回收,此時逻锐,在其方法體中定義的變量將不...
    Y了個J閱讀 4,414評論 1 14
  • Java繼承關系初始化順序 父類的靜態(tài)變量-->父類的靜態(tài)代碼塊-->子類的靜態(tài)變量-->子類的靜態(tài)代碼快-->父...
    第六象限閱讀 2,147評論 0 9
  • 轉載自:https://halfrost.com/go_map_chapter_one/ https://half...
    HuJay閱讀 6,132評論 1 5
  • 從來都沒有不勞而獲夫晌,牽引力教育UI培訓回憶錄 你們知道毛毛蟲是怎么過河的嗎雕薪? 毛毛蟲想要過河只有一種方法,那就是變...
    團子團子喲閱讀 162評論 0 0