日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

Java集合:HashMap线程不安全?有哪些表现?

發(fā)布時(shí)間:2025/3/21 java 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java集合:HashMap线程不安全?有哪些表现? 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

HashMap是線程不安全的!主要表現(xiàn)在多線程情況下:

1)hash沖突時(shí),put方法不是同步的先存的值會(huì)被后存的值覆蓋。(1.7和1.8都有的表現(xiàn))

2)在resize的時(shí)候,可能會(huì)導(dǎo)致死循環(huán)(環(huán)形鏈表)(僅1.7會(huì)有的表現(xiàn),因?yàn)槠漕^插法導(dǎo)致)


讓我們先來(lái)了解一下HashMap的底層存儲(chǔ)結(jié)構(gòu),HashMap底層是一個(gè)Entry數(shù)組,一旦發(fā)生Hash沖突的的時(shí)候,HashMap采用拉鏈法解決碰撞沖突,Entry內(nèi)部的變量:

[java]?view plain?copy

?

  • final?Object?key;??
  • Object?value;??
  • Entry?next;??
  • int?hash;??

  • ? ? ? ? 通過(guò)Entry內(nèi)部的next變量可以知道使用的是鏈表,這時(shí)候我們可以知道,如果多個(gè)線程,在某一時(shí)刻同時(shí)操作HashMap并執(zhí)行put操作,而有大于兩個(gè)key的hash值相同,如圖中a1、a2,這個(gè)時(shí)候需要解決碰撞沖突,而解決沖突的辦法上面已經(jīng)說(shuō)過(guò),對(duì)于鏈表的結(jié)構(gòu)在這里不再贅述,暫且不討論是從鏈表頭部插入還是從尾部初入,這個(gè)時(shí)候兩個(gè)線程如果恰好都取到了對(duì)應(yīng)位置的頭結(jié)點(diǎn)e1,而最終的結(jié)果可想而知,a1、a2兩個(gè)數(shù)據(jù)中勢(shì)必會(huì)有一個(gè)會(huì)丟失,如圖所示:

    ?

    再來(lái)看下put方法

    ?

    [java]?view plain?copy

    ?

  • public?Object?put(Object?obj,?Object?obj1)??
  • ????{??
  • ????????if(table?==?EMPTY_TABLE)??
  • ????????????inflateTable(threshold);??
  • ????????if(obj?==?null)??
  • ????????????return?putForNullKey(obj1);??
  • ????????int?i?=?hash(obj);??
  • ????????int?j?=?indexFor(i,?table.length);??
  • ????????for(Entry?entry?=?table[j];?entry?!=?null;?entry?=?entry.next)??
  • ????????{??
  • ????????????Object?obj2;??
  • ? ? ? ? ? ? //搜索同一個(gè)桶的鏈表上是否有相同key的entry,有則直接替換value
  • ????????????if(entry.hash?==?i?&&?((obj2?=?entry.key)?==?obj?||?obj.equals(obj2)))??
  • ????????????{??
  • ????????????????Object?obj3?=?entry.value;??
  • ????????????????entry.value?=?obj1;??
  • ????????????????entry.recordAccess(this);??
  • ????????????????return?obj3;??
  • ????????????}??
  • ????????}??
  • ??
  • ????????modCount++;??
  • ? ? ? ? //判斷是否需要擴(kuò)容,是則擴(kuò)容,否,則創(chuàng)建entry
  • ????????addEntry(i,?obj,?obj1,?j);??
  • ????????return?null;??
  • ????}??

  • put方法不是同步的,同時(shí)調(diào)用了addEntry方法:

    ?

    [java]?view plain?copy

    ?

  • void?addEntry(int?i,?Object?obj,?Object?obj1,?int?j)??
  • ????{??
  • ????????if(size?>=?threshold?&&?null?!=?table[j])??
  • ????????{??
  • ????????????resize(2?*?table.length);??
  • ????????????i?=?null?==?obj???0?:?hash(obj);??
  • ????????????j?=?indexFor(i,?table.length);??
  • ????????}??
  • ????????createEntry(i,?obj,?obj1,?j);??
  • ????}??
  • addEntry方法依然不是同步的,所以導(dǎo)致了線程不安全出現(xiàn)傷處問(wèn)題,其他類似操作不再說(shuō)明,源碼一看便知


    ?resize死循環(huán)(JDK1.7)

    重新調(diào)整 HashMap 大小的時(shí)候,存在條件競(jìng)爭(zhēng)。

    因?yàn)槿绻麅蓚€(gè)線程都發(fā)現(xiàn) HashMap 需要重新調(diào)整大小了,它們會(huì)同時(shí)試著調(diào)整大小。在調(diào)整大小的過(guò)程中,存儲(chǔ)在鏈表中的元素的次序會(huì)反過(guò)來(lái)。因?yàn)橐苿?dòng)到新的 bucket 位置的時(shí)候,HashMap 并不會(huì)將元素放在鏈表的尾部而是放在頭部。這是為了避免尾部遍歷(tail traversing)。如果條件競(jìng)爭(zhēng)發(fā)生了,那么就死循環(huán)了。多線程的環(huán)境下不使用 HashMap。

    HashMap 的容量是有限的。當(dāng)經(jīng)過(guò)多次元素插入,使得 HashMap 達(dá)到一定飽和度時(shí),Key 映射位置發(fā)生沖突的幾率會(huì)逐漸提高這時(shí)候, HashMap 需要擴(kuò)展它的長(zhǎng)度,也就是進(jìn)行Resize。

  • 擴(kuò)容:創(chuàng)建一個(gè)新的 Entry 空數(shù)組,長(zhǎng)度是原數(shù)組的2倍

  • rehash:遍歷原 Entry 數(shù)組,把所有的 Entry 重新 Hash 到新數(shù)組

  • 為什么多線程會(huì)導(dǎo)致死循環(huán),它是怎么發(fā)生的?

    我們都知道HashMap初始容量大小為16,一般來(lái)說(shuō),當(dāng)有數(shù)據(jù)要插入時(shí),都會(huì)檢查容量有沒(méi)有超過(guò)設(shè)定的thredhold,如果超過(guò),需要增大Hash表的尺寸,但是這樣一來(lái),整個(gè)Hash表里的元素都需要被重算一遍。這叫rehash,這個(gè)成本相當(dāng)?shù)拇蟆?/p>

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    void resize(int newCapacity) {

    ????????Entry[] oldTable = table;

    ????????int oldCapacity = oldTable.length;

    ????????if (oldCapacity == MAXIMUM_CAPACITY) {

    ????????????threshold = Integer.MAX_VALUE;

    ????????????return;

    ????????}

    ?

    ????????Entry[] newTable = new Entry[newCapacity];

    ????????transfer(newTable, initHashSeedAsNeeded(newCapacity));

    ????????table = newTable;

    ????????threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

    }

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    void transfer(Entry[] newTable, boolean rehash) {

    ????????int newCapacity = newTable.length;

    ????????for (Entry<K,V> e : table) {

    ????????????while(null != e) {

    ????????????????Entry<K,V> next = e.next;

    ????????????????if (rehash) {

    ????????????????????e.hash = null == e.key ? 0 : hash(e.key);

    ????????????????}

    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //頭插法(JDK1.7)

    ????????????????int i = indexFor(e.hash, newCapacity);

    ????????????????e.next = newTable[i];

    ????????????????newTable[i] = e;

    ????????????????e = next;

    ????????????}

    ????????}

    }

    ??

    大概看下transfer:

  • 對(duì)索引數(shù)組中的元素遍歷
  • 對(duì)鏈表上的每一個(gè)節(jié)點(diǎn)遍歷:用 next 取得要轉(zhuǎn)移那個(gè)元素的下一個(gè),將 e 轉(zhuǎn)移到新 Hash 表的頭部,使用頭插法插入節(jié)點(diǎn)。
  • 循環(huán)2,直到鏈表節(jié)點(diǎn)全部轉(zhuǎn)移
  • 循環(huán)1,直到所有索引數(shù)組全部轉(zhuǎn)移
  • 經(jīng)過(guò)這幾步,我們會(huì)發(fā)現(xiàn)轉(zhuǎn)移的時(shí)候是逆序的。假如轉(zhuǎn)移前鏈表順序是1->2->3,那么轉(zhuǎn)移后就會(huì)變成3->2->1。這時(shí)候就有點(diǎn)頭緒了,死鎖問(wèn)題不就是因?yàn)?->2的同時(shí)2->1造成的嗎?所以,HashMap 的死鎖問(wèn)題就出在這個(gè)transfer()函數(shù)上。

    1.1?單線程 rehash 詳細(xì)演示

    單線程情況下,rehash 不會(huì)出現(xiàn)任何問(wèn)題:

    • 假設(shè)hash算法就是最簡(jiǎn)單的 key mod table.length(也就是數(shù)組的長(zhǎng)度)。
    • 最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以后碰撞發(fā)生在 table[1]
    • 接下來(lái)的三個(gè)步驟是 Hash表 resize 到4,并將所有的?<key,value>?重新rehash到新 Hash 表的過(guò)程

    如圖所示:頭插法

    ?

    1.2?多線程 rehash 詳細(xì)演示

    為了思路更清晰,我們只將關(guān)鍵代碼展示出來(lái)

    1

    2

    3

    4

    5

    6

    while(null != e) {

    ????Entry<K,V> next = e.next;

    ????e.next = newTable[i];

    ????newTable[i] = e;

    ????e = next;

    }

  • Entry<K,V> next = e.next;——因?yàn)槭菃捂湵?#xff0c;如果要轉(zhuǎn)移頭指針,一定要保存下一個(gè)結(jié)點(diǎn),不然轉(zhuǎn)移后鏈表就丟了
  • e.next = newTable[i];——e 要插入到鏈表的頭部,所以要先用 e.next 指向新的 Hash 表第一個(gè)元素(為什么不加到新鏈表最后?因?yàn)閺?fù)雜度是 O(N))
  • newTable[i] = e;——現(xiàn)在新 Hash 表的頭指針仍然指向 e 沒(méi)轉(zhuǎn)移前的第一個(gè)元素,所以需要將新 Hash 表的頭指針指向 e
  • e = next——轉(zhuǎn)移 e 的下一個(gè)結(jié)點(diǎn)
  • 假設(shè)這里有兩個(gè)線程同時(shí)執(zhí)行了put()操作,并進(jìn)入了transfer()環(huán)節(jié)

    1

    2

    3

    4

    5

    6

    while(null != e) {

    ????Entry<K,V> next = e.next; //線程1執(zhí)行到這里被調(diào)度掛起了

    ????e.next = newTable[i];

    ????newTable[i] = e;

    ????e = next;

    }

    那么現(xiàn)在的狀態(tài)為:

    ?

    從上面的圖我們可以看到,因?yàn)榫€程1的 e 指向了 key(3),而 next 指向了 key(7),在線程2 rehash 后,就指向了線程2 rehash 后的鏈表。

    然后線程1被喚醒了:

  • 執(zhí)行e.next = newTable[i],于是 key(3)的 next 指向了線程1的新 Hash 表,因?yàn)樾?Hash 表為空,所以e.next = null,
  • 執(zhí)行newTable[i] = e,所以線程1的新 Hash 表第一個(gè)元素指向了線程2新 Hash 表的 key(3)。好了,e 處理完畢。
  • 執(zhí)行e = next,將 e 指向 next,所以新的 e 是 key(7)
  • 然后該執(zhí)行 key(3)的 next 節(jié)點(diǎn) key(7)了:

  • 現(xiàn)在的 e 節(jié)點(diǎn)是 key(7),首先執(zhí)行Entry<K,V> next = e.next,那么 next 就是 key(3)了
  • 執(zhí)行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
  • 執(zhí)行newTable[i] = e,那么線程1的新 Hash 表第一個(gè)元素變成了 key(7)
  • 執(zhí)行e = next,將 e 指向 next,所以新的 e 是 key(3)
  • 這時(shí)候的狀態(tài)圖為:

    ?

    然后又該執(zhí)行 key(7)的 next 節(jié)點(diǎn) key(3)了:

  • 現(xiàn)在的 e 節(jié)點(diǎn)是 key(3),首先執(zhí)行Entry<K,V> next = e.next,那么 next 就是 null
  • 執(zhí)行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
  • 執(zhí)行newTable[i] = e,那么線程1的新 Hash 表第一個(gè)元素變成了 key(3)
  • 執(zhí)行e = next,將 e 指向 next,所以新的 e 是 key(7)
  • 這時(shí)候的狀態(tài)如圖所示:

    ?

    很明顯,環(huán)形鏈表出現(xiàn)了!!當(dāng)然,現(xiàn)在還沒(méi)有事情,因?yàn)橄乱粋€(gè)節(jié)點(diǎn)是 null,所以transfer()就完成了,等put()的其余過(guò)程搞定后,HashMap 的底層實(shí)現(xiàn)就是線程1的新 Hash 表了。


    JDK 1.7 HashMap擴(kuò)容導(dǎo)致死循環(huán)的主要原因


    HashMap擴(kuò)容導(dǎo)致死循環(huán)的主要原因在于擴(kuò)容后鏈表中的節(jié)點(diǎn)在新的hash桶使用頭插法插入

    新的hash桶會(huì)倒置原h(huán)ash桶中的單鏈表,那么在多個(gè)線程同時(shí)擴(kuò)容的情況下就可能導(dǎo)致產(chǎn)生一個(gè)存在閉環(huán)的單鏈表,從而導(dǎo)致死循環(huán)。

    JDK 1.8 HashMap擴(kuò)容不會(huì)造成死循環(huán)的原因


    在JDK 1.8中執(zhí)行上面的擴(kuò)容死循環(huán)代碼示例就不會(huì)發(fā)生死循環(huán)。由于使用的是尾插法不會(huì)導(dǎo)致單鏈表的倒置,所以擴(kuò)容的時(shí)候不會(huì)導(dǎo)致死循環(huán)。

    通過(guò)上面的分析,不難發(fā)現(xiàn)循環(huán)的產(chǎn)生是因?yàn)樾骆湵淼捻樞蚋f的鏈表是完全相反的,所以只要保證建新鏈時(shí)還是按照原來(lái)的順序的話就不會(huì)產(chǎn)生循環(huán)。

    ?

    這里雖然JDK 1.8 中HashMap擴(kuò)容的時(shí)候不會(huì)造成死循環(huán),但是如果多個(gè)線程同時(shí)執(zhí)行put操作,可能會(huì)導(dǎo)致同時(shí)向一個(gè)單鏈表中插入數(shù)據(jù),從而導(dǎo)致數(shù)據(jù)丟失的。

    所以不論是JDK 1.7 還是 1.8,HashMap線程都是不安全的,要使用線程安全的Map可以考慮ConcurrentHashMap。
    ?

    總結(jié)

    以上是生活随笔為你收集整理的Java集合:HashMap线程不安全?有哪些表现?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。