欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時間:2023年05月05日 10:12:30   作者:arthur.dy.lee  
HashMap是采用鏈表解決Hash沖突,因為是鏈表結構,那么就很容易形成閉合的鏈路,這樣在循環(huán)的時候只要有線程對這個HashMap進行get操作就會產生死循環(huán),本文針對這個問題進行分析,需要的朋友可以參考下

并發(fā)問題的癥狀

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

從前我們的Java代碼因為一些原因使用了HashMap這個東西,但是當時的程序是單線程的,一切都沒有問題。后來,我們的程序性能有問題,所以需要變成多線程的,于是,變成多線程后到了線上,發(fā)現程序經常占了100%的CPU,查看堆棧,你會發(fā)現程序都Hang在了HashMap.get()這個方法上了,重啟程序后問題消失。但是過段時間又會來。而且,這個問題在測試環(huán)境里可能很難重現。

我們簡單的看一下我們自己的代碼,我們就知道HashMap被多個線程操作。而Java的文檔說HashMap是非線程安全的,應該用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個線程,不斷的往一個非線程安全的HashMap中put內容/get內容,put的內容很簡單,key和value都是從0自增的整數(這個put的內容做的并不好,以致于后來干擾了我分析問題的思路)。對HashMap做并發(fā)寫操作,我原以為只不過會產生臟數據的情況,但反復運行這個程序,會出現線程t1、t2被hang住的情況,多數情況下是一個線程被hang住另一個成功結束,偶爾會10個線程都被hang住。

產生這個死循環(huán)的根源在于對一個未保護的共享變量 — 一個”HashMap”數據結構的操作。當在所有操作的方法上加了”synchronized”后,一切恢復了正常。這算jvm的bug嗎?應該說不是的,這個現象很早以前就報告出來了。Sun的工程師并不認為這是bug,而是建議在這樣的場景下應采用”ConcurrentHashMap”,

CPU利用率過高一般是因為出現了出現了死循環(huán),導致部分線程一直運行,占用cpu時間。問題原因就是HashMap是非線程安全的,多個線程put的時候造成了某個key值Entry key List的死循環(huán),問題就這么產生了。

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

使用jstack工具dump出問題的那臺服務器的棧信息。死循環(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) 
共出現了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) 
共出現了3次。

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

多線程put的時候可能導致元素丟失

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

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 );
         }
     }
}

在這個方法里,將舊數組賦值給src,遍歷src,當src的元素非null時,就將src中的該元素置null,即將舊數組中的元素置null了,也就是這一句:

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

此時若有get方法訪問這個key,它取得的還是舊數組,當然就取不到其對應的value了。

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

HashMap數據結構

我需要簡單地說一下HashMap這個經典的數據結構。

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

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

所以,Hash表的尺寸和容量非常的重要。一般來說,Hash表這個容器當有數據要插入時,都會檢查容量有沒有超過設定的thredhold,如果超過,需要增大Hash表的尺寸,但是這樣一來,整個Hash表里的元素都需要被重算一遍。這叫rehash,這個成本相當的大。

HashMap的rehash源代碼

下面,我們來看一下Java的HashMap的源代碼。Put一個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<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不存在,需要增加一個結點
     addEntry(hash, key, value, i);
     return null ;
}

檢查容量是否超標:

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);
     //查看當前的size是否超過了我們設定的閾值threshold,如果超過,需要resize
     if (size++ >= threshold)
         resize( 2 * table.length);
}

新建一個更大尺寸的hash表,然后把數據從老的Hash表中遷移到新的Hash表中。

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

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

void transfer(Entry[] newTable)
{
     Entry[] src = table;
     int newCapacity = newTable.length;
     //下面這段代碼的意思是:
     //  從OldTable里摘一個元素出來,然后放到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 );
         }
     }
}

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

正常的ReHash過程

畫了個圖做了個演示。

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

在此輸入圖片描述

并發(fā)的Rehash過程

(1)假設我們有兩個線程。我用紅色和淺藍色標注了一下。我們再回頭看一下我們的 transfer代碼中的這個細節(jié):

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

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

在此輸入圖片描述

注意:因為Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。我們可以看到鏈表的順序被反轉后。

(2)線程一被調度回來執(zhí)行。

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

在此輸入圖片描述

(3)一切安好。 線程一接著工作。把key(7)摘下來,放到newTable[i]的第一個,然后把e和next往下移。

在此輸入圖片描述

(4)環(huán)形鏈接出現。 e.next = newTable[i] 導致 key(3).next 指向了 key(7)。注意:此時的key(7).next 已經指向了key(3), 環(huán)形鏈表就這樣出現了。

在此輸入圖片描述

于是,當我們的線程一調用到,HashTable.get(11)時,悲劇就出現了——Infinite Loop。

三種解決方案

Hashtable替換HashMap

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

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

Collections.synchronizedMap將HashMap包裝起來

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

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());
}

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

ConcurrentHashMap替換HashMap

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

到此這篇關于關于Java的HashMap多線程并發(fā)問題分析的文章就介紹到這了,更多相關Java的HashMap多線程并發(fā)問題內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 用Java生成二維碼并附帶文字信息

    用Java生成二維碼并附帶文字信息

    這篇文章主要介紹了用Java生成二維碼并附帶文字信息,文中有非常詳細的代碼示例,對正在學習java的小伙伴們有非常好的幫助,需要的朋友可以參考下
    2021-04-04
  • 你都理解創(chuàng)建線程池的參數嗎?

    你都理解創(chuàng)建線程池的參數嗎?

    這篇文章主要介紹了創(chuàng)建線程池參數,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-04-04
  • Spring框架的ImportSelector詳細解讀

    Spring框架的ImportSelector詳細解讀

    這篇文章主要介紹了Spring框架的ImportSelector詳細解讀,Spring中一個非常重要的注解@Import中的ImportSelector接口的作用以及它到底有啥作用,也會捎帶一部分源碼說一下DeferredImportSelector是干啥的,需要的朋友可以參考下
    2024-01-01
  • SpringBoot整合JWT(JSON?Web?Token)生成token與驗證的流程及示例

    SpringBoot整合JWT(JSON?Web?Token)生成token與驗證的流程及示例

    JSON Web Token(JWT)是一種開放的標準(RFC 7519),定義了一種緊湊的、自包含的方式來安全地在各方之間傳輸信息作為JSON對象,這篇文章主要給大家介紹了關于SpringBoot整合JWT(JSON?Web?Token)生成token與驗證的相關資料,需要的朋友可以參考下
    2024-07-07
  • Java將文件上傳到ftp服務器

    Java將文件上傳到ftp服務器

    這篇文章主要為大家詳細介紹了Java將文件上傳到ftp服務器,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-06-06
  • Spring boot項目中異常攔截設計和處理詳解

    Spring boot項目中異常攔截設計和處理詳解

    這篇文章主要介給大家紹了關于Spring boot項目中異常攔截設計和處理的相關資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用spring boot具有一定的參考學習價值,需要的朋友們下面隨著小編來一起看看吧
    2018-12-12
  • MyBatis傳入參數為List對象的實現

    MyBatis傳入參數為List對象的實現

    這篇文章主要介紹了MyBatis傳入參數為List對象的實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2021-03-03
  • JVM之內存分配和回收機制

    JVM之內存分配和回收機制

    本篇主要介紹JVM內存分配和回收策略,內容主要節(jié)選自《深入理解java虛擬機》,需要的朋友可以參考下
    2023-05-05
  • springboot項目父子多模塊打包方式

    springboot項目父子多模塊打包方式

    這篇文章主要介紹了springboot項目父子多模塊打包方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • maven中下載jar包源碼和javadoc的命令介紹

    maven中下載jar包源碼和javadoc的命令介紹

    這篇文章主要介紹了maven中下載jar包源碼和javadoc的命令介紹,本文講解了Maven命令下載源碼和javadocs、通過配置文件添加、配置eclipse等內容,需要的朋友可以參考下
    2015-03-03

最新評論