深刻解析HashMap、HashTable

集合類之番外篇:深刻解析HashMap、HashTablejava

Java集合類是個很是重要的知識點,HashMap、HashTable、ConcurrentHashMap等算是集合類中的重點,可謂「重中之重」,首先來看個問題,如面試官問你:HashMap和HashTable有什麼區別,一個比較簡單的回答是:面試

一、HashMap是非線程安全的,HashTable是線程安全的。算法

二、HashMap的鍵和值都容許有null值存在,而HashTable則不行。數組

三、由於線程安全的問題,HashMap效率比HashTable的要高。安全

能答出上面的三點,簡單的面試,算是過了,可是若是再問:Java中的另外一個線程安全的與HashMap及其相似的類是什麼?一樣是線程安全,它與HashTable在線程同步上有什麼不一樣?能把第二個問題完整的答出來,說明你的基礎算是不錯的了。帶着這個問題,本章開始系Java之美[從菜鳥到高手演變]系列之深刻解析HashMap和HashTable類應用而生!總想在文章的開頭說點兒什麼,但又無從提及。從最近的一些面試提及吧,感覺就是:知識是永無止境的,永遠不要以爲本身已經掌握了某些東西。若是對哪一塊知識感興趣,那麼,請多多的花時間,哪怕最基礎的東西也要理解它的原理,儘可能往深了研究,在學習的同時,記得多與你們交流溝通,由於也許某些東西,從你本身的角度,是很難發現的,由於你並無那麼多的實驗環境去發現他們。只有交流的多了,才能及時找出本身的不足,才能認識到:「哦,原來我還有這麼多不知道的東西!」。數據結構

1、HashMap的內部存儲結構
Java中數據存儲方式最底層的兩種結構,一種是數組,另外一種就是鏈表,數組的特色:連續空間,尋址迅速,可是在刪除或者添加元素的時候須要有較大幅度的移動,因此查詢速度快,增刪較慢。而鏈表正好相反,因爲空間不連續,尋址困難,增刪元素只需修改指針,因此查詢慢、增刪快。有沒有一種數據結構來綜合一下數組和鏈表,以便發揮他們各自的優點?答案是確定的!就是:哈希表。哈希表具備較快(常量級)的查詢速度,及相對較快的增刪速度,因此很適合在海量數據的環境中使用。通常實現哈希表的方法採用「拉鍊法」,咱們能夠理解爲「鏈表的數組」,以下圖:多線程

從上圖中,咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。通常狀況是經過hash(key)%len得到,也就是元素的key的哈希值對數組長度取模獲得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存儲在數組下標爲12的位置。它的內部實際上是用一個Entity數組來實現的,屬性有key、value、next。接下來我會從初始化階段詳細的講解HashMap的內部結構。併發

一、初始化
首先來看三個常量:
static final int DEFAULT_INITIAL_CAPACITY = 16; 初始容量:16
static final int MAXIMUM_CAPACITY = 1 
<< 30; 最大容量:2的30次方:1073741824
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
裝載因子,後面再說它的做用
來看個無參構造方法,也是咱們最經常使用的:app

[java]  view plain copy
 
 
  1. public HashMap() {  
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR;  
  3.         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
  4.         table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  5.         init();  
  6.     }  

loadFactor、threshold的值在此處沒有起到做用,不過他們在後面的擴容方面會用到,此處只需理解table=new Entry[DEFAULT_INITIAL_CAPACITY].說明,默認就是開闢16個大小的空間。另一個重要的構造方法:函數

[java]  view plain copy
 
 
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.         if (initialCapacity < 0)  
  3.             throw new IllegalArgumentException("Illegal initial capacity: " +  
  4.                                                initialCapacity);  
  5.         if (initialCapacity > MAXIMUM_CAPACITY)  
  6.             initialCapacity = MAXIMUM_CAPACITY;  
  7.         if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  8.             throw new IllegalArgumentException("Illegal load factor: " +  
  9.                                                loadFactor);  
  10.   
  11.         // Find a power of 2 >= initialCapacity  
  12.         int capacity = 1;  
  13.         while (capacity < initialCapacity)  
  14.             capacity <<= 1;  
  15.   
  16.         this.loadFactor = loadFactor;  
  17.         threshold = (int)(capacity * loadFactor);  
  18.         table = new Entry[capacity];  
  19.         init();  
  20.     }  


就是說傳入參數的構造方法,咱們把重點放在:

[java]  view plain copy
 
 
  1. while (capacity < initialCapacity)  
  2.            capacity <<= 1;  


上面,該代碼的意思是,實際的開闢的空間要大於傳入的第一個參數的值。舉個例子:
new HashMap(7,0.8),loadFactor爲0.8,capacity爲7,經過上述代碼後,capacity的值爲:8.(1 << 2的結果是4,2 << 2的結果爲8<此處感謝網友wego1234的指正>)。因此,最終capacity的值爲8,最後經過new Entry[capacity]來建立大小爲capacity的數組,因此,這種方法最紅取決於capacity的大小。
二、put(Object key,Object value)操做
 
當調用put操做時,首先判斷key是否爲null,以下代碼1處:

[java]  view plain copy
 
 
  1. <p>public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }</p><p>        modCount++;  
  15.         addEntry(hash, key, value, i);  
  16.         return null;  
  17.     }</p>  


若是key是null,則調用以下代碼:

[java]  view plain copy
 
 
  1. private V putForNullKey(V value) {  
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
  3.             if (e.key == null) {  
  4.                 V oldValue = e.value;  
  5.                 e.value = value;  
  6.                 e.recordAccess(this);  
  7.                 return oldValue;  
  8.             }  
  9.         }  
  10.         modCount++;  
  11.         addEntry(0, null, value, 0);  
  12.         return null;  
  13.     }  


就是說,獲取Entry的第一個元素table[0],並基於第一個元素的next屬性開始遍歷,直到找到key爲null的Entry,將其value設置爲新的value值。
若是沒有找到key爲null的元素,則調用如上述代碼的addEntry(0, null, value, 0);增長一個新的entry,代碼以下:

[java]  view plain copy
 
 
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     Entry<K,V> e = table[bucketIndex];  
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.         if (size++ >= threshold)  
  5.             resize(2 * table.length);  
  6.     }  


先獲取第一個元素table[bucketIndex],傳給e對象,新建一個entry,key爲null,value爲傳入的value值,next爲獲取的e對象。若是容量大於threshold,容量擴大2倍。
若是key不爲null,這也是大多數的狀況,從新看一下源碼:

[java]  view plain copy
 
 
  1. public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());//---------------2---------------  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {//--------------3-----------  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }//-------------------4------------------  
  15.         modCount++;//----------------5----------  
  16.         addEntry(hash, key, value, i);-------------6-----------  
  17.         return null;  
  18.     }  


看源碼中2處,首先會進行key.hashCode()操做,獲取key的哈希值,hashCode()是Object類的一個方法,爲本地方法,內部實現比較複雜,咱們
會在後面做單獨的關於Java中Native方法的分析中介紹。hash()的源碼以下:

[java]  view plain copy
 
 
  1. static int hash(int h) {  
  2.         // This function ensures that hashCodes that differ only by  
  3.         // constant multiples at each bit position have a bounded  
  4.         // number of collisions (approximately 8 at default load factor).  
  5.         h ^= (h >>> 20) ^ (h >>> 12);  
  6.         return h ^ (h >>> 7) ^ (h >>> 4);  
  7.     }  

int i = indexFor(hash, table.length);的意思,至關於int i = hash % Entry[].length;獲得i後,就是在Entry數組中的位置,(上述代碼5和6處是若是Entry數組中不存在新要增長的元素,則執行5,6處的代碼,若是存在,即Hash衝突,則執行 3-4處的代碼,此處HashMap中採用鏈地址法解決Hash衝突。此處經網友bbycszh指正,發現上述陳述有些問題)。從新解釋:其實無論Entry數組中i位置有無元素,都會去執行5-6處的代碼,若是沒有,則直接新增,若是有,則將新元素設置爲Entry[0],其next指針指向原有對象,即原有對象爲Entry[1]。具體方法能夠解釋爲下面的這段文字:(3-4處的代碼只是檢查在索引爲i的這條鏈上有沒有key重複的,有則替換且返回原值,程序再也不去執行5-6處的代碼,無則無處理

上面咱們提到過Entry類裏面有一個next屬性,做用是指向下一個Entry。如, 第一個鍵值對A進來,經過計算其key的hash獲得的i=0,記作:Entry[0] = A。一會後又進來一個鍵值對B,經過計算其i也等於0,如今怎麼辦?HashMap會這樣作:B.next = A,Entry[0] = B,若是又進來C,i也等於0,那麼C.next = B,Entry[0] = C;這樣咱們發現i=0的地方其實存取了A,B,C三個鍵值對,他們經過next這個屬性連接在一塊兒,也就是說數組中存儲的是最後插入的元素。

到這裏爲止,HashMap的大體實現,咱們應該已經清楚了。固然HashMap裏面也包含一些優化方面的實現,這裏也說一下。好比:Entry[]的長度必定後,隨着map裏面數據的愈來愈長,這樣同一個i的鏈就會很長,會不會影響性能?HashMap裏面設置一個因素(也稱爲因子),隨着map的size愈來愈大,Entry[]會以必定的規則加長長度。

二、get(Object key)操做
get(Object key)操做時根據鍵來獲取值,若是瞭解了put操做,get操做容易理解,先來看看源碼的實現:

[java]  view plain copy
 
 
  1. public V get(Object key) {  
  2.         if (key == null)  
  3.             return getForNullKey();  
  4.         int hash = hash(key.hashCode());  
  5.         for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.              e != null;  
  7.              e = e.next) {  
  8.             Object k;  
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))//-------------------1----------------  
  10.                 return e.value;  
  11.         }  
  12.         return null;  
  13.     }  


意思就是:一、當key爲null時,調用getForNullKey(),源碼以下:

[java]  view plain copy
 
 
  1. private V getForNullKey() {  
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
  3.             if (e.key == null)  
  4.                 return e.value;  
  5.         }  
  6.         return null;  
  7.     }  

二、當key不爲null時,先根據hash函數獲得hash值,在更具indexFor()獲得i的值,循環遍歷鏈表,若是有:key值等於已存在的key值,則返回其value。如上述get()代碼1處判斷。

總結下HashMap新增put和獲取get操做:

[java]  view plain copy
 
 
  1. //存儲時:  
  2. int hash = key.hashCode();  
  3. int i = hash % Entry[].length;  
  4. Entry[i] = value;  
  5.   
  6. //取值時:  
  7. int hash = key.hashCode();  
  8. int i = hash % Entry[].length;  
  9. return Entry[i];  

理解了就比較簡單。

此處附一個簡單的HashMap小算法應用:

[java]  view plain copy
 
 
  1. package com.xtfggef.hashmap;  
  2.   
  3. import java.util.HashMap;  
  4. import java.util.Map;  
  5. import java.util.Set;  
  6.   
  7. /** 
  8.  * 打印在數組中出現n/2以上的元素 
  9.  * 利用一個HashMap來存放數組元素及出現的次數 
  10.  * @author erqing 
  11.  * 
  12.  */  
  13. public class HashMapTest {  
  14.       
  15.     public static void main(String[] args) {  
  16.           
  17.         int [] a = {2,3,2,2,1,4,2,2,2,7,9,6,2,2,3,1,0};  
  18.           
  19.         Map<Integer, Integer> map = new HashMap<Integer,Integer>();  
  20.         for(int i=0; i<a.length; i++){  
  21.             if(map.containsKey(a[i])){  
  22.                 int tmp = map.get(a[i]);  
  23.                 tmp+=1;  
  24.                 map.put(a[i], tmp);  
  25.             }else{  
  26.                 map.put(a[i], 1);  
  27.             }  
  28.         }  
  29.         Set<Integer> set = map.keySet();//------------1------------  
  30.         for (Integer s : set) {  
  31.             if(map.get(s)>=a.length/2){  
  32.                 System.out.println(s);  
  33.             }  
  34.         }//--------------2---------------  
  35.     }  
  36. }  

此處注意兩個地方,map.containsKey(),還有就是上述1-2處的代碼。

理解了HashMap的上面的操做,其它的大多數方法都很容易理解了。搞清楚它的內部存儲機制,一切OK!

2、HashTable的內部存儲結構

HashTable和HashMap採用相同的存儲機制,兩者的實現基本一致,不一樣的是:

一、HashMap是非線程安全的,HashTable是線程安全的,內部的方法基本都是synchronized。

二、HashTable不容許有null值的存在。

在HashTable中調用put方法時,若是key爲null,直接拋出NullPointerException。其它細微的差異還有,好比初始化Entry數組的大小等等,但基本思想和HashMap同樣。

3、HashTable和ConcurrentHashMap的比較

如我開篇所說同樣,ConcurrentHashMap是線程安全的HashMap的實現。一樣是線程安全的類,它與HashTable在同步方面有什麼不一樣呢?

以前咱們說,synchronized關鍵字加鎖的原理,實際上是對對象加鎖,不論你是在方法前加synchronized仍是語句塊前加,鎖住的都是對象總體,可是ConcurrentHashMap的同步機制和這個不一樣,它不是加synchronized關鍵字,而是基於lock操做的,這樣的目的是保證同步的時候,鎖住的不是整個對象。事實上,ConcurrentHashMap能夠知足concurrentLevel個線程併發無阻塞的操做集合對象。關於concurrentLevel稍後介紹。

一、構造方法

爲了容易理解,咱們先從構造函數提及。ConcurrentHashMap是基於一個叫Segment數組的,其實和Entry相似,以下:

[java]  view plain copy
 
 
  1. public ConcurrentHashMap()  
  2.   {  
  3.     this(16, 0.75F, 16);  
  4.   }  


默認傳入值16,調用下面的方法:

[java]  view plain copy
 
 
  1. public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  
  2.   {  
  3.     if ((paramFloat <= 0F) || (paramInt1 < 0) || (paramInt2 <= 0))  
  4.       throw new IllegalArgumentException();  
  5.   
  6.     if (paramInt2 > 65536) {  
  7.       paramInt2 = 65536;  
  8.     }  
  9.   
  10.     int i = 0;  
  11.     int j = 1;  
  12.     while (j < paramInt2) {  
  13.       ++i;  
  14.       j <<= 1;  
  15.     }  
  16.     this.segmentShift = (32 - i);  
  17.     this.segmentMask = (j - 1);  
  18.     this.segments = Segment.newArray(j);  
  19.   
  20.     if (paramInt1 > 1073741824)  
  21.       paramInt1 = 1073741824;  
  22.     int k = paramInt1 / j;  
  23.     if (k * j < paramInt1)  
  24.       ++k;  
  25.     int l = 1;  
  26.     while (l < k)  
  27.       l <<= 1;  
  28.   
  29.     for (int i1 = 0; i1 < this.segments.length; ++i1)  
  30.       this.segments[i1] = new Segment(l, paramFloat);  
  31.   }  


你會發現比HashMap的構造函數多一個參數,paramInt1就是咱們以前談過的initialCapacity,就是數組的初始化大小,paramfloat爲loadFactor(裝載因子),而paramInt2則是咱們所要說的concurrentLevel,這三個值分別被初始化爲16,0.75,16,通過:

[java]  view plain copy
 
 
  1. while (j < paramInt2) {  
  2.       ++i;  
  3.       j <<= 1;  
  4.     }  


後,j就是咱們最終要開闢的數組的size值,當paramInt1爲16時,計算出來的size值就是16.經過:

this.segments = Segment.newArray(j)後,咱們看出了,最終稿建立的Segment數組的大小爲16.最終建立Segment對象時:

[java]  view plain copy
 
 
  1. this.segments[i1] = new Segment(cap, paramFloat);  

須要cap值,而cap值來源於:

[java]  view plain copy
 
 
  1. int k = paramInt1 / j;  
  2.   if (k * j < paramInt1)  
  3.     ++k;  
  4.   int cap = 1;  
  5.   while (cap < k)  
  6.     cap <<= 1;  

組後建立大小爲cap的數組。最後根據數組的大小及paramFloat的值算出了threshold的值:

this.threshold = (int)(paramArrayOfHashEntry.length * this.loadFactor)。

二、put操做

[java]  view plain copy
 
 
  1. public V put(K paramK, V paramV)  
  2.   {  
  3.     if (paramV == null)  
  4.       throw new NullPointerException();  
  5.     int i = hash(paramK.hashCode());  
  6.     return segmentFor(i).put(paramK, i, paramV, false);  
  7.   }  


與HashMap不一樣的是,若是key爲null,直接拋出NullPointer異常,以後,一樣先計算hashCode的值,再計算hash值,不過此處hash函數和HashMap中的不同:

[java]  view plain copy
 
 
  1. private static int hash(int paramInt)  
  2.   {  
  3.     paramInt += (paramInt << 15 ^ 0xFFFFCD7D);  
  4.     paramInt ^= paramInt >>> 10;  
  5.     paramInt += (paramInt << 3);  
  6.     paramInt ^= paramInt >>> 6;  
  7.     paramInt += (paramInt << 2) + (paramInt << 14);  
  8.     return (paramInt ^ paramInt >>> 16);  
  9.   }  


 

[java]  view plain copy
 
 
  1. final Segment<K, V> segmentFor(int paramInt)  
  2.   {  
  3.     return this.segments[(paramInt >>> this.segmentShift & this.segmentMask)];  
  4.   }  


根據上述代碼找到Segment對象後,調用put來操做:

[java]  view plain copy
 
 
  1. V put(K paramK, int paramInt, V paramV, boolean paramBoolean)  
  2. {  
  3.   lock();  
  4.   try {  
  5.     Object localObject1;  
  6.     Object localObject2;  
  7.     int i = this.count;  
  8.     if (i++ > this.threshold)  
  9.       rehash();  
  10.     ConcurrentHashMap.HashEntry[] arrayOfHashEntry = this.table;  
  11.     int j = paramInt & arrayOfHashEntry.length - 1;  
  12.     ConcurrentHashMap.HashEntry localHashEntry1 = arrayOfHashEntry[j];  
  13.     ConcurrentHashMap.HashEntry localHashEntry2 = localHashEntry1;  
  14.     while ((localHashEntry2 != null) && (((localHashEntry2.hash != paramInt) || (!(paramK.equals(localHashEntry2.key)))))) {  
  15.       localHashEntry2 = localHashEntry2.next;  
  16.     }  
  17.   
  18.     if (localHashEntry2 != null) {  
  19.       localObject1 = localHashEntry2.value;  
  20.       if (!(paramBoolean))  
  21.         localHashEntry2.value = paramV;  
  22.     }  
  23.     else {  
  24.       localObject1 = null;  
  25.       this.modCount += 1;  
  26.       arrayOfHashEntry[j] = new ConcurrentHashMap.HashEntry(paramK, paramInt, localHashEntry1, paramV);  
  27.       this.count = i;  
  28.     }  
  29.     return localObject1;  
  30.   } finally {  
  31.     unlock();  
  32.   }  
  33. }  


先調用lock(),lock是ReentrantLock類的一個方法,用當前存儲的個數+1來和threshold比較,若是大於threshold,則進行rehash,將當前的容量擴大2倍,從新進行hash。以後對hash的值和數組大小-1進行按位於操做後,獲得當前的key須要放入的位置,從這兒開始,和HashMap同樣。

從上述的分析看出,ConcurrentHashMap基於concurrentLevel劃分出了多個Segment來對key-value進行存儲,從而避免每次鎖定整個數組,在默認的狀況下,容許16個線程併發無阻塞的操做集合對象,儘量地減小併發時的阻塞現象。

在多線程的環境中,相對於HashTable,ConcurrentHashMap會帶來很大的性能提高!

歡迎讀者批評指正,有任何建議請聯繫:

EGG:xtfggef@gmail.com      http://weibo.com/xtfggef

4、HashMap常見問題分析

一、此處我以爲網友huxb23@126的一篇文章說的很好,分析多線程併發寫HashMap線程被hang住的緣由 ,由於是優秀的資源,此處我整理下搬到這兒。

如下內容轉自博文:http://blog.163.com/huxb23@126/blog/static/625898182011211318854/ 

先看原問題代碼:

[java]  view plain copy
 
 
  1. import java.util.HashMap;  
  2.   
  3. public class TestLock {  
  4.   
  5.     private HashMap map = new HashMap();  
  6.   
  7.     public TestLock() {  
  8.         Thread t1 = new Thread() {  
  9.             public void run() {  
  10.                 for (int i = 0; i < 50000; i++) {  
  11.                     map.put(new Integer(i), i);  
  12.                 }  
  13.                 System.out.println("t1 over");  
  14.             }  
  15.         };  
  16.   
  17.         Thread t2 = new Thread() {  
  18.             public void run() {  
  19.                 for (int i = 0; i < 50000; i++) {  
  20.                     map.put(new Integer(i), i);  
  21.                 }  
  22.   
  23.                 System.out.println("t2 over");  
  24.             }  
  25.         };  
  26.   
  27.         t1.start();  
  28.         t2.start();  
  29.   
  30.     }  
  31.   
  32.     public static void main(String[] args) {  
  33.         new TestLock();  
  34.     }  
  35. }  


就是啓了兩個線程,不斷的往一個非線程安全的HashMap中put內容,put的內容很簡單,key和value都是從0自增的整數(這個put的內容作的並很差,以至於後來干擾了我分析問題的思路)。對HashMap作併發寫操做,我原覺得只不過會產生髒數據的狀況,但反覆運行這個程序,會出現線程t一、t2被hang住的狀況,多數狀況下是一個線程被hang住另外一個成功結束,偶爾會兩個線程都被hang住。說到這裏,你若是以爲很差好學習ConcurrentHashMap而在這瞎折騰就手下留情跳過吧。
好吧,分析下HashMap的put函數源碼看看問題出在哪,這裏就羅列出相關代碼(jdk1.6):

[java]  view plain copy
 
 
  1. public V put(K paramK, V paramV)  
  2. {  
  3.   if (paramK == null)  
  4.     return putForNullKey(paramV);  
  5.   int i = hash(paramK.hashCode());  
  6.   int j = indexFor(i, this.table.length);  
  7.   for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next)  
  8.   {  
  9.     if (localEntry.hash == i) { java.lang.Object localObject1;  
  10.       if (((localObject1 = localEntry.key) == paramK) || (paramK.equals(localObject1))) {  
  11.         java.lang.Object localObject2 = localEntry.value;  
  12.         localEntry.value = paramV;  
  13.         localEntry.recordAccess(this);  
  14.         return localObject2;  
  15.       }  
  16.     }  
  17.   }  
  18.   this.modCount += 1;  
  19.   addEntry(i, paramK, paramV, j);  
  20.   return null;  
  21. }  
  22.   
  23. private V putForNullKey(V paramV)  
  24. {  
  25.   for (Entry localEntry = this.table[0]; localEntry != null; localEntry = localEntry.next)  
  26.     if (localEntry.key == null) {  
  27.       java.lang.Object localObject = localEntry.value;  
  28.       localEntry.value = paramV;  
  29.       localEntry.recordAccess(this);  
  30.       return localObject;  
  31.     }  
  32.   
  33.   this.modCount += 1;  
  34.   addEntry(0, null, paramV, 0);  
  35.   return null;  
  36. }  

 

經過jconsole(或者thread dump),能夠看到線程停在了transfer方法的while循環處。這個transfer方法的做用是,當Map中元素數超過閾值須要resize時,它負責把原Map中的元素映射到新Map中。我修改了HashMap,加上了@標記2和@標記3的代碼片段,以打印出死循環時的狀態,結果死循環線程老是出現相似這樣的輸出:「Thread-1,e==next:false,e==next.next:true,e:108928=108928,next:108928=108928,eq:true」。
這個輸出代表:
1)這個Entry鏈中的兩個Entry之間的關係是:e=e.next.next,形成死循環。
2)e.equals(e.next),但e!=e.next。由於測試例子中兩個線程put的內容同樣,併發時可能同一個key被保存了多個value,這種錯誤是在addEntry函數產生的,但這和線程死循環沒有關係。

接下來就分析transfer中那個while循環了。先所說這個循環正常的功能:src[j]保存的是映射成同一個hash值的多個Entry的鏈表,這個src[j]可能爲null,可能只有一個Entry,也可能由多個Entry連接起來。假設是多個Entry,原來的鏈是(src[j]=a)->b(也就是src[j]=a,a.next=b,b.next=null),通過while處理後獲得了(newTable[i]=b)->a。也就是說,把鏈表的next關係反向了。

再看看這個while中可能在多線程狀況下引發問題的語句。針對兩個線程t1和t2,這裏它們可能的產生問題的執行序列作些我的分析:

1)假設同一個Entry列表[e->f->...],t1先到,t2後到並都走到while中。t1執行「e.next = newTable[i];newTable[i] = e;」這使得e.next=null(初始的newTable[i]爲null),newTable[i]指向了e。這時t2執行了「e.next = newTable[i];newTable[i] = e;」,這使得e.next=e,e死循環了。由於循環開始處的「final Entry next = e.next;」,儘管e本身死循環了,在最後的「e = next;」後,兩個線程都會跳過e繼續執行下去。

2)在while中逐個遍歷Entry鏈表中的Entry而把next關係反向時,newTable[i]成爲了被交換的引用,可疑的語句在於「e.next = newTable[i];」。假設鏈表e->f->g被t1處理成e<-f<-g,newTable[i]指向了g,這時t2進來了,它一執行「e.next = newTable[i];」就使得e->g,形成了死循環。因此,理論上來講,死循環的Entry個數可能不少。儘管產生了死循環,可是t1執行到了死循環的右邊,因此是會繼續執行下去的,而t2若是執行「final Entry next = e.next;」的next爲null,則也會繼續執行下去,不然就進入了死循環。

3)彷佛狀況會更復雜,由於即使線程跳出了死循環,它下一次作resize進入transfer時,有可能由於以前的死循環Entry鏈表而被hang住(彷佛是必定會被hang住)。也有可能,在put檢查Entry鏈表時(@標記1),由於Entry鏈表的死循環而被hang住。也彷佛有可能,活着的線程和死循環的線程同時執行在while裏後,兩個線程都能活着出去。因此,可能兩個線程平安退出,可能一個線程hang在transfer中,可能兩個線程都被hang住而又不必定在一個地方。

4)我反覆的測試,出現一個線程被hang住的狀況最多,都是e=e.next.next形成的,這主要就是例子put兩份增量數據形成的。我若是去掉@標記3的輸出,有時也能復現兩個線程都被hang住的狀況,但加上後就很難復現出來。我又把put的數據改了下,好比讓兩個線程put範圍不一樣的數據,就能復現出e=e.next,兩個線程都被hang住的狀況。

上面羅哩羅嗦了不少,一開始我簡單的分析後以爲彷佛明白了怎麼回事,可如今仔細琢磨後彷佛又不明白了許多。有一個細節是,每次死循環的key的大小也是有據可循的,我就不打哈了。感受,若是樣本多些,可能出現問題的緣由點會不少,也會更復雜,我姑且再也不蛋疼下去。至於有人提到ConcurrentHashMap也有這個問題,我以爲不大可能,由於它的put操做是加鎖的,若是有這個問題就不叫線程安全的Map了。

二、HashMap中Value能夠相同,可是鍵不能夠相同

當插入HashMap的key相同時,會覆蓋原有的Value,且返回原Value值,看下面的程序:

[java]  view plain copy
 
 
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) {  
  4.           
  5.         HashMap<String,Integer> map = new HashMap<String,Integer>();  
  6.   
  7.         //出入兩個Value相同的值,沒有問題  
  8.         map.put("egg", 1);  
  9.         map.put("niu", 1);  
  10.           
  11.         //插入key相同的值,看返回結果  
  12.         int egg = (Integer) map.put("egg", 3);  
  13.           
  14.         System.out.println(egg);   //輸出1  
  15.         System.out.println(map.get("egg"));   //輸出3,將原值1覆蓋  
  16.         System.out.println(map.get("niu"));   //輸出1  
  17.     }  
  18. }  

相同的鍵會被覆蓋,且返回原值。

三、HashMap按值排序

給定一個數組,求出每一個數據出現的次數並按照次數的由大到小排列出來。咱們選用HashMap來作,key存儲數組元素,值存儲出現的次數,最後用Collections的sort方法對HashMap的值進行排序。代碼以下:

[java]  view plain copy
 
 
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) {  
  4.   
  5.         int data[] = { 2, 5, 2, 3, 5, 2, 3, 5, 2, 3, 5, 2, 3, 5, 2,  
  6.                 7, 8, 8, 7, 8, 7, 9, 0 };  
  7.         Map<Integer, Integer> map = new HashMap<Integer, Integer>();  
  8.         for (int i : data) {  
  9.             if (map.containsKey(i)) {//判斷HashMap裏是否存在  
  10.                 map.put(i, map.get(i) + 1);//已存在,值+1  
  11.             } else {  
  12.                 map.put(i, 1);//不存在,新增  
  13.             }  
  14.         }  
  15.         //map按值排序  
  16.         List<Map.Entry<Integer, Integer>> list = new ArrayList<Map.Entry<Integer, Integer>>(  
  17.                 map.entrySet());  
  18.         Collections.sort(list, new Comparator<Map.Entry<Integer, Integer>>() {  
  19.             public int compare(Map.Entry<Integer, Integer> o1,  
  20.                     Map.Entry<Integer, Integer> o2) {  
  21.                 return (o2.getValue() - o1.getValue());  
  22.             }  
  23.         });  
  24.         for (Map.Entry<Integer, Integer> m : list) {  
  25.             System.out.println(m.getKey() + "-" + m.getValue());  
  26.         }  
  27.     }  
  28.   
  29. }  

輸出:

2-6
5-5
3-4
8-3
7-3
9-1
0-1

轉自:http://blog.csdn.NET/zhangerqing/article/details/8193118

相關文章
相關標籤/搜索