<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          HashMap線程的不安全體現(xiàn)在哪兒?

          共 11751字,需瀏覽 24分鐘

           ·

          2021-02-27 11:33

          不點藍字,我們哪來故事?

          每天 11 點更新文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領(lǐng)!》

          前言:我們都知道HashMap是線程不安全的,在多線程環(huán)境中不建議使用,但是其線程不安全主要體現(xiàn)在什么地方呢,本文將對該問題進行解密。

          1.jdk1.7中的HashMap

          在jdk1.8中對HashMap做了很多優(yōu)化,這里先分析在jdk1.7中的問題,相信大家都知道在jdk1.7多線程環(huán)境下HashMap容易出現(xiàn)死循環(huán),這里我們先用代碼來模擬出現(xiàn)死循環(huán)的情況:

          public class HashMapTest {

              public static void main(String[] args) {
                  HashMapThread thread0 = new HashMapThread();
                  HashMapThread thread1 = new HashMapThread();
                  HashMapThread thread2 = new HashMapThread();
                  HashMapThread thread3 = new HashMapThread();
                  HashMapThread thread4 = new HashMapThread();
                  thread0.start();
                  thread1.start();
                  thread2.start();
                  thread3.start();
                  thread4.start();
              }
          }

          class HashMapThread extends Thread {
              private static AtomicInteger ai = new AtomicInteger();
              private static Map<Integer, Integer> map = new HashMap<>();

              @Override
              public void run() {
                  while (ai.get() < 1000000) {
                      map.put(ai.get(), ai.get());
                      ai.incrementAndGet();
                  }
              }
          }

          上述代碼比較簡單,就是開多個線程不斷進行put操作,并且HashMap與AtomicInteger都是全局共享的。在多運行幾次該代碼后,出現(xiàn)如下死循環(huán)情形:其中有幾次還會出現(xiàn)數(shù)組越界的情況:這里我們著重分析為什么會出現(xiàn)死循環(huán)的情況,通過jps和jstack命名查看死循環(huán)情況,結(jié)果如下:從堆棧信息中可以看到出現(xiàn)死循環(huán)的位置,通過該信息可明確知道死循環(huán)發(fā)生在HashMap的擴容函數(shù)中,根源在transfer函數(shù)中,jdk1.7中HashMap的transfer函數(shù)如下:

          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);
                          }
                          int i = indexFor(e.hash, newCapacity);
                          e.next = newTable[i];
                          newTable[i] = e;
                          e = next;
                      }
                  }
              }

          總結(jié)下該函數(shù)的主要作用:

          在對table進行擴容到newTable后,需要將原來數(shù)據(jù)轉(zhuǎn)移到newTable中,注意10-12行代碼,這里可以看出在轉(zhuǎn)移元素的過程中,使用的是頭插法,也就是鏈表的順序會翻轉(zhuǎn),這里也是形成死循環(huán)的關(guān)鍵點。下面進行詳細分析。

          1.1 擴容造成死循環(huán)分析過程

          前提條件:

          這里假設(shè)

          1. hash算法為簡單的用key mod鏈表的大小。
          2. 最開始hash表size=2,key=3,7,5,則都在table[1]中。
          3. 然后進行resize,使size變成4。

          未resize前的數(shù)據(jù)結(jié)構(gòu)如下:如果在單線程環(huán)境下,最后的結(jié)果如下:這里的轉(zhuǎn)移過程,不再進行詳述,只要理解transfer函數(shù)在做什么,其轉(zhuǎn)移過程以及如何對鏈表進行反轉(zhuǎn)應(yīng)該不難。

          然后在多線程環(huán)境下,假設(shè)有兩個線程A和B都在進行put操作。線程A在執(zhí)行到transfer函數(shù)中第11行代碼處掛起,因為該函數(shù)在這里分析的地位非常重要,因此再次貼出來。此時線程A中運行結(jié)果如下:線程A掛起后,此時線程B正常執(zhí)行,并完成resize操作,結(jié)果如下:這里需要特別注意的點:由于線程B已經(jīng)執(zhí)行完畢,根據(jù)Java內(nèi)存模型,現(xiàn)在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。

          此時切換到線程A上,在線程A掛起時內(nèi)存中值如下:e=3,next=7,newTable[3]=null,代碼執(zhí)行過程如下:

          newTable[3]=e ----> newTable[3]=3
          e=next ----> e=7

          此時結(jié)果如下:繼續(xù)循環(huán):

          e=7
          next=e.next ----> next=3【從主存中取值】
          e.next=newTable[3] ----> e.next=3【從主存中取值】
          newTable[3]=e ----> newTable[3]=7
          e=next ----> e=3

          結(jié)果如下:再次進行循環(huán):

          e=3
          next=e.next ----> next=null
          e.next=newTable[3] ----> e.next=7 即:3.next=7
          newTable[3]=e ----> newTable[3]=3
          e=next ----> e=null

          注意此次循環(huán):e.next=7,而在上次循環(huán)中7.next=3,出現(xiàn)環(huán)形鏈表,并且此時e=null循環(huán)結(jié)束。

          結(jié)果如下:在后續(xù)操作中只要涉及輪詢hashmap的數(shù)據(jù)結(jié)構(gòu),就會在這里發(fā)生死循環(huán),造成悲劇。

          1.2 擴容造成數(shù)據(jù)丟失分析過程

          遵照上述分析過程,初始時:線程A和線程B進行put操作,同樣線程A掛起:此時線程A的運行結(jié)果如下:此時線程B已獲得CPU時間片,并完成resize操作:同樣注意由于線程B執(zhí)行完成,newTable和table都為最新值:5.next=null。

          此時切換到線程A,在線程A掛起時:e=7,next=5,newTable[3]=null。

          執(zhí)行newtable[i]=e,就將7放在了table[3]的位置,此時next=5。接著進行下一次循環(huán):

          e=5
          next=e.next ----> next=null,從主存中取值
          e.next=newTable[1] ----> e.next=5,從主存中取值
          newTable[1]=e ----> newTable[1]=5
          e=next ----> e=null

          將5放置在table[1]位置,此時e=null循環(huán)結(jié)束,3元素丟失,并形成環(huán)形鏈表。并在后續(xù)操作hashmap時造成死循環(huán)。

          2.jdk1.8中HashMap

          在jdk1.8中對HashMap進行了優(yōu)化,在發(fā)生hash碰撞,不再采用頭插法方式,而是直接插入鏈表尾部,因此不會出現(xiàn)環(huán)形鏈表的情況,但是在多線程的情況下仍然不安全,這里我們看jdk1.8中HashMap的put操作源碼:

          final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                             boolean evict)
           
          {
                  Node<K,V>[] tab; Node<K,V> p; int n, i;
                  if ((tab = table) == null || (n = tab.length) == 0)
                      n = (tab = resize()).length;
                  if ((p = tab[i = (n - 1) & hash]) == null// 如果沒有hash碰撞則直接插入元素
                      tab[i] = newNode(hash, key, value, null);
                  else {
                      Node<K,V> e; K k;
                      if (p.hash == hash &&
                          ((k = p.key) == key || (key != null && key.equals(k))))
                          e = p;
                      else if (p instanceof TreeNode)
                          e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                      else {
                          for (int binCount = 0; ; ++binCount) {
                              if ((e = p.next) == null) {
                                  p.next = newNode(hash, key, value, null);
                                  if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
                                      treeifyBin(tab, hash);
                                  break;
                              }
                              if (e.hash == hash &&
                                  ((k = e.key) == key || (key != null && key.equals(k))))
                                  break;
                              p = e;
                          }
                      }
                      if (e != null) { // existing mapping for key
                          V oldValue = e.value;
                          if (!onlyIfAbsent || oldValue == null)
                              e.value = value;
                          afterNodeAccess(e);
                          return oldValue;
                      }
                  }
                  ++modCount;
                  if (++size > threshold)
                      resize();
                  afterNodeInsertion(evict);
                  return null;
              }

          這是jdk1.8中HashMap中put操作的主函數(shù), 注意第6行代碼,如果沒有hash碰撞則會直接插入元素。如果線程A和線程B同時進行put操作,剛好這兩條不同的數(shù)據(jù)hash值一樣,并且該位置數(shù)據(jù)為null,所以這線程A、B都會進入第6行代碼中。

          假設(shè)一種情況,線程A進入后還未進行數(shù)據(jù)插入時掛起,而線程B正常執(zhí)行,從而正常插入數(shù)據(jù),然后線程A獲取CPU時間片,此時線程A不用再進行hash判斷了,問題出現(xiàn):線程A會把線程B插入的數(shù)據(jù)給覆蓋,發(fā)生線程不安全。

          這里只是簡要分析下jdk1.8中HashMap出現(xiàn)的線程不安全問題的體現(xiàn),后續(xù)將會對java的集合框架進行總結(jié),到時再進行具體分析。

          總結(jié)

          首先HashMap是線程不安全的,其主要體現(xiàn):

          1. 在jdk1.7中,在多線程環(huán)境下,擴容時會造成環(huán)形鏈或數(shù)據(jù)丟失。

          2. 在jdk1.8中,在多線程環(huán)境下,會發(fā)生數(shù)據(jù)覆蓋的情況。

          往期推薦

          一個 HTTP 打趴 80% 面試者

          VS Code 真的會一統(tǒng)江湖嗎?

          干飯時間到,補貼大戰(zhàn)再起!

          阿里巴巴達摩院三周年,立的 flag 有沒有被打臉?

          下方二維碼關(guān)注我

          技術(shù)草根堅持分享 編程,算法,架構(gòu)

          看完文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領(lǐng)!》

          朋友,助攻一把!點個在看
          瀏覽 40
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线一区二区三区四区 | 五月天婷婷啪啪 | 波多野结衣一级婬片A片免费下载 | 亚洲成人无码在线播放 | 欧美色图亚洲图片插菊花综合 |