国产片侵犯亲女视频播放_亚洲精品二区_在线免费国产视频_欧美精品一区二区三区在线_少妇久久久_在线观看av不卡

服務(wù)器之家:專注于服務(wù)器技術(shù)及軟件下載分享
分類導(dǎo)航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術(shù)|正則表達(dá)式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務(wù)器之家 - 編程語言 - Java教程 - 關(guān)于Java的HashMap多線程并發(fā)問題分析

關(guān)于Java的HashMap多線程并發(fā)問題分析

2023-05-08 01:05未知服務(wù)器之家 Java教程

目錄 并發(fā)問題的癥狀 多線程put后可能導(dǎo)致get死循環(huán) 多線程put的時(shí)候可能導(dǎo)致元素丟失 put非null元素后get出來的卻是null HashMap數(shù)據(jù)結(jié)構(gòu) HashMap的rehash源代碼 正常的ReHash過程 并發(fā)的Rehash過程 三種解決方案 Hashtable替換HashMap Collections

目錄
  • 并發(fā)問題的癥狀
    • 多線程put后可能導(dǎo)致get死循環(huán)
    • 多線程put的時(shí)候可能導(dǎo)致元素丟失
    • put非null元素后get出來的卻是null
  • HashMap數(shù)據(jù)結(jié)構(gòu)
    • HashMap的rehash源代碼
      • 正常的ReHash過程
        • 并發(fā)的Rehash過程
          • 三種解決方案
            • Hashtable替換HashMap
            • Collections.synchronizedMap將HashMap包裝起來
            • ConcurrentHashMap替換HashMap

          并發(fā)問題的癥狀

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

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

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

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

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

          使用jstack工具dump出問題的那臺(tái)服務(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)然就取不到其對(duì)應(yīng)的value了。

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

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

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

          HashMap通常會(huì)用一個(gè)指針數(shù)組(假設(shè)為table[])來做分散所有的key,當(dāng)一個(gè)key被加入時(shí),會(huì)通過Hash算法通過key算出這個(gè)數(shù)組的下標(biāo)i,然后就把這個(gè) 插到table[i]中,如果有兩個(gè)不同的key被算在了同一個(gè)i,那么就叫沖突,又叫碰撞,這樣會(huì)在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í),都會(huì)檢查容量有沒有超過設(shè)定的thredhold,如果超過,需要增大Hash表的尺寸,但是這樣一來,整個(gè)Hash表里的元素都需要被重算一遍。這叫rehash,這個(gè)成本相當(dāng)?shù)拇蟆?/p>

          HashMap的rehash源代碼

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

          public V put(K key, V value)
          {
               ......
               //算Hash值
               int hash = hash(key.hashCode());
               int i = indexFor(hash, table.length);
               //如果該key已被插入,則替換掉舊的value (鏈接操作)
               for (Entry<K,V> 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<K,V> e = table[bucketIndex];
               table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
               //查看當(dāng)前的size是否超過了我們?cè)O(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++) {
                   Entry<K,V> e = src[j];
                   if (e != null ) {
                       src[j] = null ;
                       do {
                           Entry<K,V> 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算法就是簡(jiǎn)單的用key mod 一下表的大小(也就是數(shù)組的長(zhǎng)度)。
          2. 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都沖突在table1這里了。
          3. 接下來的三個(gè)步驟是Hash表 resize成4,然后所有的 重新rehash的過程。

          關(guān)于Java的HashMap多線程并發(fā)問題分析

          而我們的線程二執(zhí)行完成了。于是我們有下面的這個(gè)樣子。

          關(guān)于Java的HashMap多線程并發(fā)問題分析

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

          ConcurrentHashMap替換HashMap

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

          原文地址:https://blog.csdn.net/paincupid/article/details/51241783

          延伸 · 閱讀

          精彩推薦
          主站蜘蛛池模板: 日韩欧美在线一区二区 | 亚洲精品无 | 激情小视频| 超色视频在线观看 | 日韩中文字幕视频在线 | 日本免费在线 | 女人久久久久久久 | 国产精品2区| 男女羞羞网站 | 亚洲国产一区二区在线观看 | 一区二区福利 | 亚洲人视频在线 | 亚洲永久免费视频 | 日韩精品一区二区三区av | 99精品欧美一区二区三区综合在线 | 激情综合在线观看 | 亚洲精品视频在线 | 国产91在线观看 | 中国大陆一级毛片 | 亚洲国产精品久久 | 日韩视频在线观看 | 国产色秀视频在线观看 | 日日摸夜夜添夜夜添精品视频 | 久久久久久国产精品高清 | 五月婷婷激情网 | 欧美成人a∨高清免费观看 亚洲国产精品尤物yw在线观看 | 亚洲久草 | 韩国精品一区 | 亚洲影音 | 特级西西人体4444xxxx | 国产毛片在线 | 色九九| 黄色av影视 | 国精品一区 | 国产精品ssss在线亚洲 | a级黄色在线观看 | 国产资源视频在线观看 | 隔壁老王国产在线精品 | 自拍偷拍精品 | 91精品国产91久久久久久吃药 | 成人片免费看 |