Java集合——HashMap、HashTable以及ConCurrentHashMap異同比較

0. 前言java

 

HashMap和HashTable的區別一種比較簡單的回答是:面試

(1)HashMap是非線程安全的,HashTable是線程安全的。算法

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

(3)由於線程安全、哈希效率的問題,HashMap效率比HashTable的要高。安全

可是若是繼續追問:Java中的另外一個線程安全的與HashMap功能極其相似的類是什麼?數據結構

一樣是線程安全,它與HashTable在線程同步上有什麼不一樣?帶着這些問題,開始今天的文章。多線程

本文爲原創,相關內容會持續維護,轉載請標明出處:http://blog.csdn.net/seu_calvin/article/details/52653711併發

 

1.  HashMap概述函數

 

Java中的數據存儲方式有兩種結構,一種是數組,另外一種就是鏈表,前者的特色是連續空間,尋址迅速,可是在增刪元素的時候會有較大幅度的移動,因此數組的特色是查詢速度快,增刪較慢。性能

而鏈表因爲空間不連續,尋址困難,增刪元素只需修改指針,因此鏈表的特色是查詢速度慢、增刪快。

那麼有沒有一種數據結構來綜合一下數組和鏈表以便發揮他們各自的優點?答案就是哈希表。哈希表的存儲結構以下圖所示:

 

從上圖中,咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點,經過功能相似於hash(key.hashCode())%len的操做,得到要添加的元素所要存放的的數組位置。

HashMap的哈希算法實際操做是經過位運算,比取模運算效率更高,一樣能達到使其分佈均勻的目的,後面會介紹。

鍵值對所存放的數據結構實際上是HashMap中定義的一個Entity內部類,數組來實現的,屬性有key、value和指向下一個Entity的next。

 

 

2.  HashMap初始化

 

HashMap有兩種經常使用的構造方法:

第一種是不須要參數的構造方法:

static final int DEFAULT_INITIAL_CAPACITY = 16; //初始數組長度爲16  
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量爲2的30次方  
//裝載因子用來衡量HashMap滿的程度  
//計算HashMap的實時裝載因子的方法爲:size/capacity  
static final float DEFAULT_LOAD_FACTOR = 0.75f; //裝載因子  
  
public HashMap() {    
    this.loadFactor = DEFAULT_LOAD_FACTOR;    
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
//默認數組長度爲16   
    table = new Entry[DEFAULT_INITIAL_CAPACITY];  
    init();    
}  
 

第二種是須要參數的構造方法:

public HashMap(int initialCapacity, float loadFactor) {    
        if (initialCapacity < 0)    
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);    
        if (initialCapacity > MAXIMUM_CAPACITY)    
            initialCapacity = MAXIMUM_CAPACITY;    
        if (loadFactor <= 0 || Float.isNaN(loadFactor))    
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);    
  
        // Find a power of 2 >= initialCapacity    
        int capacity = 1;    
        while (capacity < initialCapacity)    
            capacity <<= 1;    
    
        this.loadFactor = loadFactor;    
        threshold = (int)(capacity * loadFactor);    
        table = new Entry[capacity];    
        init();    
}   
 

從源碼能夠看出,初始化的數組長度爲capacity,capacity的值老是2的N次方,大小比第一個參數稍大或相等。

 

3.  HashMap的put操做

public V put(K key, V value) {    
        if (key == null)    
          return putForNullKey(value);    
        int hash = hash(key.hashCode());    
        int i = indexFor(hash, table.length);    
        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++;    
        addEntry(hash, key, value, i);    
        return null;    
}  
 

3.1  put進的key爲null

 

private V putForNullKey(V value) {    
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
            if (e.key == null) {    
                V oldValue = e.value;    
                e.value = value;    
                e.recordAccess(this);    
                return oldValue;    
            }    
        }    
        modCount++;    
        addEntry(0, null, value, 0);    
        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);    
        if (size++ >= threshold)    
            resize(2 * table.length);    
    } 

從源碼中能夠看出,HashMap是容許key爲null的,會調用putForNullKey()方法:

 

putForNullKey方法會遍歷以table[0]爲鏈表頭的鏈表,若是存在key爲null的KV,那麼替換其value值並返回舊值。不然調用addEntry方法,這個方法也很簡單,將[null,value]放在table[0]的位置,並將新加入的鍵值對封裝成一個Entity對象,將其next指向原table[0]處的Entity實例。

 

size表示HashMap中存放的全部鍵值對的數量。

threshold = capacity*loadFactor,最後幾行代碼表示當HashMap的size大於threshold時會執行resize操做,將HashMap擴容爲原來的2倍。擴容須要從新計算每一個元素在數組中的位置,indexFor()方法中的table.length參數也證實了這一點。

可是擴容是一個很是消耗性能的操做,因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。好比說咱們有1000個元素,那麼咱們就該聲明new HashMap(2048),由於須要考慮默認的0.75的擴容因子和數組數必須是2的N次方。若使用聲明new HashMap(1024)那麼put過程當中會進行擴容。

 

3.2  put進的key不爲null

將上述put方法中的相關代碼複製一下方便查看:

int hash = hash(key.hashCode());    
int i = indexFor(hash, table.length);    
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++;    
addEntry(hash, key, value, i);    
return null;    
}
 

從源碼能夠看出,第一、2行計算將要put進的鍵值對的數組的位置i。第4行判斷加入的key是否和以table[i]爲鏈表頭的鏈表中全部的鍵值對有重複,若重複則替換value並返回舊值,若沒有重複則調用addEntry方法,上面對這個方法的邏輯已經介紹過了。

至此HashMap的put操做已經介紹完畢了。

 

4.  HashMap的get操做

public V get(Object key) {    
   if (key == null)    
       return getForNullKey();    
   int hash = hash(key.hashCode());    
   for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {    
            Object k;    
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
                return e.value;    
        }    
    return null;    
}    
  
private V getForNullKey() {    
   for (Entry<K,V> e = table[0]; e != null; e = e.next) {    
   if (e.key == null)    
     return e.value;    
    }    
    return null;    
}  

 

 

若是瞭解了前面的put操做,那麼這裏的get操做邏輯就很容易理解了,源碼中的邏輯已經很是很是清晰了。

須要注意的只有當找不到對應value時,返回的是null。或者value自己就是null。這是能夠經過containsKey()來具體判斷。

 

瞭解了上面HashMap的put和get操做原理,能夠經過下面這個小例題進行知識鞏固,題目是打印在數組中出現n/2以上的元素,咱們即可以使用HashMap的特性來解決。

public class HashMapTest {    
    public static void main(String[] args) {    
        int [] a = {2,1,3,2,0,4,2,1,2,3,1,5,6,2,2,3};    
        Map<Integer, Integer> map = new HashMap<Integer,Integer>();    
        for(int i=0; i<a.length; i++){    
            if(map.containsKey(a[i])){    
                int tmp = map.get(a[i]);    
                tmp+=1;    
                map.put(a[i], tmp);    
            }else{    
                map.put(a[i], 1);    
            }    
        }    
        Set<Integer> set = map.keySet();          
for (Integer s : set) {    
            if(map.get(s)>=a.length/2){    
                System.out.println(s);    
            }    
        }  
    }    
} 

5.  HashMap和HashTable的對比

 

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

(1)HashMap是非線程安全的,HashTable是線程安全的,內部的方法基本都通過synchronized修飾。

(2)由於同步、哈希性能等緣由,性能確定是HashMap更佳,所以HashTable已被淘汰。

(3) HashMap容許有null值的存在,而在HashTable中put進的鍵值只要有一個null,直接拋出NullPointerException。

(4)HashMap默認初始化數組的大小爲16,HashTable爲11。前者擴容時乘2,使用位運算取得哈希,效率高於取模。然後者爲乘2加1,都是素數和奇數,這樣取模哈希結果更均勻。

這裏原本我沒有仔細看二者的具體哈希算法過程,打算粗略比較一下區別就過的,可是最近師姐面試美團移動開發時被問到了稍微具體一些的算法過程,我也是醉了…不過仍是恭喜師姐面試成功,起薪20W,真是羨慕,但願本身一年後找工做也能順順利利的。

言歸正傳,看下兩種集合的hash算法。看源碼也不難理解。

//HashMap的散列函數,這裏傳入參數爲鍵值對的key  
static final int hash(Object key) {  
    int h;  
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
}   
//返回hash值的索引,h & (length-1)操做等價於 hash % length操做, 但&操做性能更優  
static int indexFor(int h, int length) {  
    // length must be a non-zero power of 2  
    return h & (length-1);  
}  
  
//HashTable的散列函數直接在put方法裏實現了  
int hash = key.hashCode();  
int index = (hash & 0x7FFFFFFF) % tab.length;  

 

 

 

 

6.  HashTable和ConCurrentHashMap的對比

 

先對ConcurrentHashMap進行一些介紹吧,它是線程安全的HashMap的實現。

HashTable裏使用的是synchronized關鍵字,這實際上是對對象加鎖,鎖住的都是對象總體,當Hashtable的大小增長到必定的時候,性能會急劇降低,由於迭代時須要被鎖定很長的時間。

ConcurrentHashMap算是對上述問題的優化,其構造函數以下,默認傳入的是16,0.75,16。

public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  {    
    //
    int i = 0;    
    int j = 1;    
    while (j < paramInt2) {    
      ++i;    
      j <<= 1;    
    }    
    this.segmentShift = (32 - i);    
    this.segmentMask = (j - 1);    
    this.segments = Segment.newArray(j);    
    //
    int k = paramInt1 / j;    
    if (k * j < paramInt1)    
      ++k;    
    int l = 1;    
    while (l < k)    
      l <<= 1;    
    
    for (int i1 = 0; i1 < this.segments.length; ++i1)    
      this.segments[i1] = new Segment(l, paramFloat);    
  }    
  
public V put(K paramK, V paramV)  {    
    if (paramV == null)    
      throw new NullPointerException();    
    int i = hash(paramK.hashCode()); //這裏的hash函數和HashMap中的不同  
    return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);    
}  

 

ConcurrentHashMap引入了分割(Segment),上面代碼中的最後一行其實就能夠理解爲把一個大的Map拆分紅N個小的HashTable,在put方法中,會根據hash(paramK.hashCode())來決定具體存放進哪一個Segment,若是查看Segment的put操做,咱們會發現內部使用的同步機制是基於lock操做的,這樣就能夠對Map的一部分(Segment)進行上鎖,這樣影響的只是將要放入同一個Segment的元素的put操做,保證同步的時候,鎖住的不是整個Map(HashTable就是這麼作的),相對於HashTable提升了多線程環境下的性能,所以HashTable已經被淘汰了。

 

7.  HashMap和ConCurrentHashMap的對比

最後對這倆兄弟作個區別總結吧:

(1)通過4.2的分析,咱們知道ConcurrentHashMap對整個桶數組進行了分割分段(Segment),而後在每個分段上都用lock鎖進行保護,相對於HashTable的syn關鍵字鎖的粒度更精細了一些,併發性能更好,而HashMap沒有鎖機制,不是線程安全的。

(2)HashMap的鍵值對容許有null,可是ConCurrentHashMap都不容許。

相關文章
相關標籤/搜索