集合類HashMap,HashTable,ConcurrentHashMap區別?

1.HashMap

1524282042(1)

簡單來講,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,若是定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操做很快,僅需一次尋址便可;若是定位到的數組包含鏈表,對於添加操做,其時間複雜度依然爲O(1),由於最新的Entry會插入鏈表頭部,僅需簡單改變引用鏈便可,而對於查找操做來說,此時就須要遍歷鏈表,而後經過key對象的equals方法逐一比對查找。因此,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。算法

hash函數(對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘可能分佈均勻)數組

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}安全

查找函數bash

1524283341(1)

在JDK1.8 對hashmap作了改造,以下圖數據結構

shixinzhang

JDK 1.8 之前 HashMap 的實現是 數組+鏈表,即便哈希函數取得再好,也很難達到元素百分百均勻分佈。併發

當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就至關於一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間複雜度就是 O(n),徹底失去了它的優點。函數

針對這種狀況,JDK 1.8 中引入了 紅黑樹(查找時間複雜度爲 O(logn))來優化這個問題性能

2.HashTable

Hashtable它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。優化

  • table是一個 Entry[] 數組類型,而 Entry(在 HashMap 中有講解過)實際上就是一個單向鏈表。哈希表的」key-value鍵值對」都是存儲在Entry數組中的。
  • count 是 Hashtable 的大小,它是 Hashtable 保存的鍵值對的數量。
  • threshold 是 Hashtable 的閾值,用於判斷是否須要調整 Hashtable 的容量。threshold 的值=」容量*加載因子」。
  • loadFactor 就是加載因子。
  • modCount 是用來實現 fail-fast 機制的。

put 方法

put 方法的整個流程爲:ui

  1. 判斷 value 是否爲空,爲空則拋出異常;
  2. 計算 key 的 hash 值,並根據 hash 值得到 key 在 table 數組中的位置 index,若是 table[index] 元素不爲空,則進行迭代,若是遇到相同的 key,則直接替換,並返回舊 value;
  3. 不然,咱們能夠將其插入到 table[index] 位置。
public synchronized V put(K key, V value) {
        // Make sure the value is not null確保value不爲null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        //確保key不在hashtable中
        //首先,經過hash方法計算key的哈希值,並計算得出index值,肯定其在table[]中的位置
        //其次,迭代index索引位置的鏈表,若是該位置處的鏈表存在相同的key,則替換value,返回舊的value
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                V old = e.value;
                e.value = value;
                return old;
            }
        }

        modCount++;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            //若是超過閥值,就進行rehash操做
            rehash();

            tab = table;
            hash = hash(key);
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        //將值插入,返回的爲null
        Entry<K,V> e = tab[index];
        // 建立新的Entry節點,並將新的Entry插入Hashtable的index位置,並設置e爲新的Entry的下一個元素
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
        return null;
    }複製代碼

get 方法

相比較於 put 方法,get 方法則簡單不少。其過程就是首先經過 hash()方法求得 key 的哈希值,而後根據 hash 值獲得 index 索引(上述兩步所用的算法與 put 方法都相同)。而後迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。

public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
    }複製代碼

3.ConcurrentHashMap

在JDK1.7版本中,ConcurrentHashMap的數據結構是由一個Segment數組和多個HashEntry組成

Segment數組的意義就是將一個大的table分割成多個小的table來進行加鎖,也就是上面的提到的鎖分離技術,而每個Segment元素存儲的是HashEntry數組+鏈表,這個和HashMap的數據存儲結構同樣

put操做

對於ConcurrentHashMap的數據插入,這裏要進行兩次Hash去定位數據的存儲位置

static class Segment<K,V> extends ReentrantLock implements Serializable {

從上Segment的繼承體系能夠看出,Segment實現了ReentrantLock,也就帶有鎖的功能,當執行put操做時,會進行第一次key的hash來定位Segment的位置,若是該Segment尚未初始化,即經過CAS操做進行賦值,而後進行第二次hash操做,找到相應的HashEntry的位置,這裏會利用繼承過來的鎖的特性,在將數據插入指定的HashEntry位置時(鏈表的尾端),會經過繼承ReentrantLock的tryLock()方法嘗試去獲取鎖,若是獲取成功就直接插入相應的位置,若是已經有線程獲取該Segment的鎖,那當前線程會以自旋的方式去繼續的調用tryLock()方法去獲取鎖,超過指定次數就掛起,等待喚醒。

get操做

ConcurrentHashMap的get操做跟HashMap相似,只是ConcurrentHashMap第一次須要通過一次hash定位到Segment的位置,而後再hash定位到指定的HashEntry,遍歷該HashEntry下的鏈表進行對比,成功就返回,不成功就返回null。

計算ConcurrentHashMap的元素大小是一個有趣的問題,由於他是併發操做的,就是在你計算size的時候,他還在併發的插入數據,可能會致使你計算出來的size和你實際的size有相差(在你return size的時候,插入了多個數據),要解決這個問題,JDK1.7版本用兩種方案。

  1. 第一種方案他會使用不加鎖的模式去嘗試屢次計算ConcurrentHashMap的size,最多三次,比較先後兩次計算的結果,結果一致就認爲當前沒有元素加入,計算的結果是準確的;
  2. 第二種方案是若是第一種方案不符合,他就會給每一個Segment加上鎖,而後計算ConcurrentHashMap的size返回。

JDK1.8的實現

JDK1.8的實現已經摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,併發控制使用Synchronized和CAS來操做,整個看起來就像是優化過且線程安全的HashMap,雖然在JDK1.8中還能看到Segment的數據結構,可是已經簡化了屬性,只是爲了兼容舊版本。

put操做

在上面的例子中咱們新增我的信息會調用put方法,咱們來看下。

  1. 若是沒有初始化就先調用initTable()方法來進行初始化過程
  2. 若是沒有hash衝突就直接CAS插入
  3. 若是還在進行擴容操做就先進行擴容
  4. 若是存在hash衝突,就加鎖來保證線程安全,這裏有兩種狀況,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入,
  5. 最後一個若是該鏈表的數量大於閾值8,就要先轉換成黑紅樹的結構,break再一次進入循環
  6. 若是添加成功就調用addCount()方法統計size,而且檢查是否須要擴容

get操做

咱們如今要回到開始的例子中,咱們對我的信息進行了新增以後,咱們要獲取所新增的信息,使用String name = map.get(「name」)獲取新增的name信息,如今咱們依舊用debug的方式來分析下ConcurrentHashMap的獲取方法get()

public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); //計算兩次hash
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1 ) & h)) != null ) { //讀取首節點的Node元素
if ((eh = e.hash) == h) { //若是該節點就是首節點就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值爲負值表示正在擴容,這個時候查的是ForwardingNode的find方法來定位到nextTable來
//查找,查找到就返回
else if (eh < 0 )
return (p = e.find(h, key)) != null ? p.val : null ;
while ((e = e.next) != null ) { //既不是首節點也不是ForwardingNode,那就往下遍歷
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null ;
}
  1. 計算hash值,定位到該table索引位置,若是是首節點符合就返回
  2. 若是遇到擴容的時候,會調用標誌正在擴容節點ForwardingNode的find方法,查找該節點,匹配就返回
  3. 以上都不符合的話,就往下遍歷節點,匹配就返回,不然最後就返回null

其實能夠看出JDK1.8版本的ConcurrentHashMap的數據結構已經接近HashMap,相對而言,ConcurrentHashMap只是增長了同步的操做來控制併發,從JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+紅黑樹,相對而言,總結以下思考:

  1. JDK1.8的實現下降鎖的粒度,JDK1.7版本鎖的粒度是基於Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是HashEntry(首節點)
  2. JDK1.8版本的數據結構變得更加簡單,使得操做也更加清晰流暢,由於已經使用synchronized來進行同步,因此不須要分段鎖的概念,也就不須要Segment這種數據結構了,因爲粒度的下降,實現的複雜度也增長了
  3. JDK1.8使用紅黑樹來優化鏈表,基於長度很長的鏈表的遍歷是一個很漫長的過程,而紅黑樹的遍歷效率是很快的,代替必定閾值的鏈表,這樣造成一個最佳拍檔
  4. JDK1.8爲何使用內置鎖synchronized來代替重入鎖ReentrantLock,我以爲有如下幾點:
  • 由於粒度下降了,在相對而言的低粒度加鎖方式,synchronized並不比ReentrantLock差,在粗粒度加鎖中ReentrantLock可能經過Condition來控制各個低粒度的邊界,更加的靈活,而在低粒度中,Condition的優點就沒有了
  • JVM的開發團隊歷來都沒有放棄synchronized,並且基於JVM的synchronized優化空間更大,使用內嵌的關鍵字比使用API更加天然
  • 在大量的數據操做下,對於JVM的內存壓力,基於API的ReentrantLock會開銷更多的內存,雖然不是瓶頸,可是也是一個選擇依據

4.總結

Hashtable和HashMap有幾個主要的不一樣:線程安全以及速度。僅在你須要徹底的線程安全的時候使用Hashtable,而若是你使用Java 5或以上的話,請使用ConcurrentHashMap吧。

相關文章
相關標籤/搜索