HashMap、ConcurrentHashMap

HashMap的實現原理(JDK1.7):

一. 數據結構

public interface Map<K,V> {

   interface Entry<K,V> {

   K getKey();

   V getValue();

   ... ...

   }

}
public class HashMap<K,V> extends AbstractMap<K,V>{

  static class Node<K,V> implements Map.Entry<K,V> {

        final int hash;

        final K key;

        V value;

        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {

            this.hash = hash;

            this.key = key;

            this.value = value;

            this.next = next;

        }

        public final K getKey()        { return key; }

        public final V getValue()      { return value; }

        public final String toString() { return key + "=" + value; }

  }
}

HashMap中的Node<K,V> 實現了Map.Entry,將每個key-value對維護在其中,並將全部的key-value對維護在Node<K,V>[] table數組中。html

二.方法

  1. put方法:
public V put(K key, V value) 
 { 
	 // 若是 key 爲 null,調用 putForNullKey 方法進行處理
	 if (key == null) 
		 return putForNullKey(value); 
	 // 根據 key 的 keyCode 計算 Hash 值
	 int hash = hash(key.hashCode()); 
	 // 搜索指定 hash 值在對應 table 中的索引
 	 int i = indexFor(hash, table.length);
	 // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素
	 for (Entry<K,V> e = table[i]; e != null; e = e.next) 
	 { 
		 Object k; 
		 // 找到指定 key 與須要放入的 key 相等(hash 值相同
		 // 經過 equals 比較放回 true),則覆蓋原Entry
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
		 { 
			 V oldValue = e.value; 
			 e.value = value; 
			 e.recordAccess(this); 
			 return oldValue; 
		 } 
	 } 
	 // 若是 i 索引處的 Entry 爲 null,代表此處尚未 Entry ,將新的Entry放在此處
     //又若是,hashCode相等,但equals返回,則將新的Entry與舊的Entry放在一塊兒造成鏈
	 modCount++; 
	 // 將 key、value 添加到 i 索引處
	 addEntry(hash, key, value, i); 
	 return null; 
 }

當向 HashMap 中添加 key-value 對,由其 key 的 hashCode() 返回值決定該 key-value 對(就是 Entry 對象)的存儲位置。當兩個 Entry 對象的 key 的 hashCode() 返回值相同時,將由 key 經過 eqauls() 比較值決定是採用覆蓋行爲(返回 true),仍是產生 Entry 鏈(返回 false)。這就是解決hash衝突的方法。java

void addEntry(int hash, K key, V value, int bucketIndex) 
{ 
    // 獲取指定 bucketIndex 索引處的 Entry 
    Entry<K,V> e = table[bucketIndex]; 	 // ①
    // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
    // 若是 Map 中的 key-value 對的數量超過了極限
    if (size++ >= threshold) 
        // 把 table 對象的長度擴充到 2 倍。
        resize(2 * table.length); 	 // ②
}

上面方法的代碼很簡單,但其中包含了一個很是優雅的設計:系統老是將新添加的 Entry 對象放入 table 數組的 bucketIndex 索引處——若是 bucketIndex 索引處已經有了一個 Entry 對象,那新添加的 Entry 對象指向原有的 Entry 對象(產生一個 Entry 鏈),若是 bucketIndex 索引處沒有 Entry 對象,也就是上面程序①號代碼的 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是沒有產生 Entry 鏈。數組

  1. get方法
public V get(Object key) 
 { 
	 // 若是 key 是 null,調用 getForNullKey 取出對應的 value 
	 if (key == null) 
		 return getForNullKey(); 
	 // 根據該 key 的 hashCode 值計算它的 hash 碼
	 int hash = hash(key.hashCode()); 
	 // 直接取出 table 數組中指定索引處的值,
	 for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
		 e != null; 
		 // 搜索該 Entry 鏈的下一個 Entr 
		 e = e.next) 		 // ①
	 { 
		 Object k; 
		 // 若是該 Entry 的 key 與被搜索 key 相同
		 if (e.hash == hash && ((k = e.key) == key 
			 || key.equals(k))) 
			 return e.value; 
	 } 
	 return null; 
 }

從上面代碼中能夠看出,若是 HashMap 的每一個 bucket 裏只有一個 Entry 時,HashMap 能夠根據索引、快速地取出該 bucket 裏的 Entry;在發生「Hash 衝突」的狀況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每一個 Entry,直到找到想搜索的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那系統必須循環到最後才能找到該元素。安全

  1. 爲何HashMap的大小要是2的指數次 ?
static int indexFor(int h, int length)   
{   
    return h & (length-1);   
}

key通過hash後,能夠取模來進行放入數組,也不會出現越界的狀況,之因此沒有使用取模,而是按位與的形式,是由於計算機的二進制運算效率比取模效率高。若是Map的大小不是2的進制,咱們設置爲7,7的二進制是:111,(length-1)大小是6,按位與是和6進行,6的二進制是:110,結果以下,有些數組中的位置沒有被設置,有些重複了,一是致使空間浪費,同時增長了碰撞的概率。數據結構

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

/>>>是無符號右移的意思,這裏的做用是將hashCode和本身的高16位作^運算。因爲在計算下標時要h & (length-1),而length通常都小於2^16即小於65536。 h & (length-1)的結果始終是h的低16位與(length-1)進行&運算。因此爲了讓h的低16位更隨機,故選擇讓其和高16位作^運算。多線程

  1. 擴容機制

初始容量是16,擴容因子是0.75,擴容時大小變爲原來2倍。0.75是時間和空間成本上一種折衷:增大負載因子能夠減小 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的的操做(HashMap 的 get() 與 put() 方法都要用到查詢);減少負載因子會提升數據查詢的性能,但會增長 Hash 表所佔用的內存空間。併發

觸發擴容時,會先申請一個2倍於原Node數組大小的數組,而後依次遍歷原數組,並從新計算每一個元素在新數組中對應的下標。這樣還會致使同一個數組位置上的鏈表在新數組對應的鏈表中變爲逆序。 less

併發擴容引發的環(死鏈): https://www.cnblogs.com/wang-meng/p/7582532.htmldom

三.歷HashMap的三種方式:

//1. map.Entry是Map的一個內部接口,entrySet()的返回值是一個Set集合,此集合的類型爲Map.Entry——Set<Entry<String, String>>。
Map map = new HashMap();
Iterator iterator = map.entrySet().iterator();
while(iterator.hasNext()) {
	Map.Entry entry = iterator.next();
	Object key = entry.getKey();
}

//2
Map map = new HashMap();
Set  keySet= map.keySet();
Iterator iterator = keySet.iterator;
while(iterator.hasNext()) {
	Object key = iterator.next();
	Object value = map.get(key);
}

//3
Map map = new HashMap();
Collection c = map.values();
Iterator iterator = c.iterator();
while(iterator.hasNext()) {
	Object value = iterator.next();
}

HashMap的實現原理(JDK1.8,和JDK1.7的區別):

  1. 當發生hash衝突時,向鏈表中以尾插法插入ide

  2. 當節點的鏈表長度超過8之後,轉化爲紅黑樹(同時數組容量必須大於64(至少也是4x8=32)時才轉化爲紅黑樹,不然觸發擴容。這麼作是爲了防止在數組容量較小的初期,多個值插入同一位置致使引發沒必要要的轉化)。

    閾值設置爲8的緣由是,理想狀態下哈希表的每一個箱子中,元素的數量遵照泊松分佈,元素個數超過8的機率極小。 而當鏈表中的元素個數小於6時,紅黑樹退化爲鏈表。設置爲6而不是7,是爲了防止當鏈表容量爲7時,頻繁的插入和刪除一個元素致使頻繁的進行鏈表和紅黑樹之間的轉化。

  3. 擴容時,再也不像1.7那樣從新計算每一個元素的新的下標 h&(length-1),而是進行以下判斷:

    擴容時遍歷每個舊元素,一樣用尾插法插入新數組,能夠避免出現逆序和死鏈的狀況

ConcurrentHashMap的實現原理(JDK1.7):

http://www.javashuo.com/article/p-aquxfzkq-gq.html

一.數據結構

static final class HashEntry<K,V> { 
            final K key;                 // 聲明 key 爲 final 型
            final int hash;              // 聲明 hash 值爲 final 型 
            volatile V value;           // 聲明 value 爲 volatile 型
            final HashEntry<K,V> next;  // 聲明 next 爲 final 型 
 
 
            HashEntry(K key, int hash, HashEntry<K,V> next, V value)  { 
                this.key = key; 
                this.hash = hash; 
                this.next = next; 
                this.value = value; 
            } 
     }

ConcurrentHashMap會建立16個Segment,Segment繼承ReentrantLock,每一個Segment包含一個HashEntry/[/]數組

static final class Segment<K,V> extends ReentrantLock implements Serializable {  
     private static final long serialVersionUID = 2249069246763182397L;  
             /** 
              * 在本 segment 範圍內,包含的 HashEntry 元素的個數
              * 該變量被聲明爲 volatile 型,保證每次讀取到最新的數據
              */  
             transient volatile int count;  
 
 
             /** 
              *table 被更新的次數
              */  
             transient int modCount;  
 
 
             /** 
              * 當 table 中包含的 HashEntry 元素的個數超過本變量值時,觸發 table 的再散列
              */  
             transient int threshold;  
 
 
             /** 
              * table 是由 HashEntry 對象組成的數組
              * 若是散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式連接成一個鏈表
              * table 數組的數組成員表明散列映射表的一個桶
              * 每一個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分
              * 若是併發級別爲 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16 
              */  
             transient volatile HashEntry<K,V>[] table;  
 
 
             /** 
              * 裝載因子
              */  
             final float loadFactor;  
    }

二.方法

  1. put() 加鎖

先根據hashCode找到Segment,再根據hashCode找到數組下標。

ConcurrentHashMap和HashTable同樣不容許key或value爲null。經過get(k)獲取對應的value時,若是獲取到的是null時,你沒法判斷,它是put(k,v)的時候value爲null,仍是這個key歷來沒有作過映射。HashMap是非併發的,能夠經過contains(key)來作這個判斷。而支持併發的Map在調用m.contains(key)和m.get(key)時,m可能已經不一樣了。

  1. get()
V get(Object key, int hash) { 
        if(count != 0) {       // 首先讀 count 變量
            HashEntry<K,V> e = getFirst(hash); 
            while(e != null) { 
                if(e.hash == hash && key.equals(e.key)) { 
                    V v = e.value; 
                    if(v != null)            
                        return v; 
                    // 若是讀到 value 域爲 null,說明發生了重排序,或者get了一個剛插入的entry,而這個entry還沒構造好。加鎖後從新讀取
                    return readValueUnderLock(e); 
                } 
                e = e.next; 
            } 
        } 
        return null; 
    }
     V readValueUnderLock(HashEntry<K,V> e) {  
         lock();  
         try {  
             return e.value;  
         } finally {  
             unlock();  
         }  
     }

因爲next是final類型,因此插入新元素只能採用頭插法。若是get()的元素正好是別的線程剛插入的,可能還未初始化完成,返回null,因此須要加鎖後再獲取一次.

因爲next是final類型,因此刪除元素只能經過從新構造一個鏈表的方法:將待刪除節點前面的結點複製一遍,尾結點指向待刪除節點的下一個結點。 待刪除節點後面的結點不須要複製,它們能夠重用。

若是咱們get的節點是e3,可能咱們順着鏈表剛找到e1,這時另外一個線程就執行了刪除e3的操做,而咱們線程還會繼續沿着舊的鏈表找到e3返回。這裏沒有辦法實時保證了。不過這也沒什麼關係,即便咱們返回e3的時候,它被其餘線程刪除了,暴漏出去的e3也不會對咱們新的鏈表形成影響。

  1. size()

count是volitel類型變量,由於在累加count操做過程當中,以前累加過的count發生變化的概率很是小,因此ConcurrentHashMap的作法是先嚐試2次經過不鎖住Segment的方式來統計各個Segment大小,若是統計的過程當中,容器的count發生了變化,則再採用加鎖的方式來統計全部Segment的大小。

那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢?使用modCount變量,在put , remove和clean方法裏操做元素前都會將變量modCount進行加1,那麼在統計size先後比較modCount是否發生變化,從而得知容器的大小是否發生變化。

ConcurrentHashMap的實現原理(JDK1.8):

  1. put():

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

在put時,若是沒有hash衝突,則以CAS方式插入;若是有hash衝突,則用Synchronized加鎖。

  1. size():

JDK1.8的ConcurrentHashMap使用baseCount以及CounterCell[]數組統計容量的大小。原本可使用CAS的方式更新一個表示容量的字段,可是爲了保證高併發下的效率,做者使用CounterCell[]數組的方式,用多個CAS的方式去更新多個表示容量的字段,最後再將這些字段加和表示map的總容量。

size()方法調用sumCount()計算總容量:

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

能夠看出sumCount()就是將baseCount和CounterCell[]中各元素求和。

在put()、remove()等方法中會調用addCount()去修改map的容量:

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //第一次執行到這裏,CounterCell爲空,是否執行if內的語句取決於baseCount是否能cas累加成功
    //若是永遠沒有併發,則永遠只累加baseCount。一旦有併發產生,就會初始化CounterCell,再也不累加baseCount
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        //無競爭
        boolean uncontended = true;
        //第一次執行到這裏,as爲空,直接執行if內語句進行CounterCell的初始化
        if (as == null || (m = as.length - 1) < 0 ||
            //probe至關於線程的hash碼,這裏判斷當前線程的CounterCell是否初始化過,不然初始化,是則累加對應的cellValue
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            //初始化CounterCell
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
}

能夠看出,在沒有併發的狀況下,只會累加baseCount的值;當有衝突時,纔會調用fullAddCount()初始化CounterCell[]:

private final void fullAddCount(long x, boolean wasUncontended) {
    int h;
    //初始化線程的探針
    if ((h = ThreadLocalRandom.getProbe()) == 0) {
        ThreadLocalRandom.localInit();
        h = ThreadLocalRandom.getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        //第一次counterCells爲空
        //以後counterCells不爲空
        CounterCell[] as; CounterCell a; int n; long v;
        if ((as = counterCells) != null && (n = as.length) > 0) {
            //當前線程對應的CounterCell槽位爲空,初始化槽位
            if ((a = as[(n - 1) & h]) == null) {
                //cas
                if (cellsBusy == 0) {            // Try to attach new Cell
                    CounterCell r = new CounterCell(x); // Optimistic create
                    //cas
                    if (cellsBusy == 0 &&
                        U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                        boolean created = false;
                        try {               // Recheck under lock
                            CounterCell[] rs; int m, j;
                            if ((rs = counterCells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            //槽位不爲空,再次cas累加cellValue。成功則退出,失敗則繼續往下執行
            else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                break;
            //CounterCell容量是否超過cpu核心數,否,則接更新collide標誌,使下次再進行到此處時執行擴容
            else if (counterCells != as || n >= NCPU)
                collide = false;            // At max size or stale
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 &&
                     U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                try {
                    //擴容爲原來的2倍
                    if (counterCells == as) {// Expand table unless stale
                        CounterCell[] rs = new CounterCell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        counterCells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            //更新probe
            h = ThreadLocalRandom.advanceProbe(h);
        }
        //CELLSBUSY是一個cas方式的自旋鎖,旨在初始化CounterCell[]
        else if (cellsBusy == 0 && counterCells == as &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            boolean init = false;
            try {
                if (counterCells == as) {
                    CounterCell[] rs = new CounterCell[2];
                    rs[h & 1] = new CounterCell(x);
                    counterCells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        //獲取cas鎖失敗
        else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
            break;                          // Fall back on using base
    }
}

參考https://www.sohu.com/a/254192521_355142

其實能夠看出JDK1.8版本的ConcurrentHashMap的數據結構已經接近HashMap:

  • JDK1.7版本鎖的粒度是基於Segment的,包含多個HashEntry,而JDK1.8鎖的粒度就是HashEntry(首節點);
  • JDK1.8版本的數據結構變得更加簡單,使得操做也更加清晰流暢,由於已經使用synchronized來進行同步,因此不須要分段鎖的概念,也就不須要Segment這種數據結構了;
  • JVM的開發團隊歷來都沒有放棄synchronized,並且基於JVM的synchronized優化空間更大,使用內嵌的關鍵字比使用API更加天然;
  • 在大量的數據操做下,對於JVM的內存壓力,基於API的ReentrantLock會開銷更多的內存;

HashMap與HashTable

1.不一樣點:

HashMap和Hashtable都實現了map接口。

它們的不一樣點有:

  1. HashMap容許鍵和值時null,而Hashtable不容許鍵或值爲null.
  2. Hashtable是同步的,跟適合多線程。HashMap不是,更適合單線程。
  3. HashMap提供了可供迭代的鍵的集合,所以,HashMap是快速失敗的。另外一方面,Hashtable提供了對鍵的枚舉(Enumeration),是安全失敗的。
  4. 擴容的參數不同,HashMap要保證每次擴容後是2的次方倍,Hashtable是擴大一倍。
  5. 散列計算不一樣。HashMap由於容量是2的次方倍,因此使用減1與的散列方式而非取餘,優化了散列速度。Hashtable是直接取餘。

2.爲什麼HashMap容許鍵和值時null,而Hashtable不容許鍵或值爲null:

Hashtable:

if (value == null) {
            throw new NullPointerException();
        }
Entry<?,?> tab[] = table;
int hash = key.hashCode();

value爲null時直接拋錯,而計算key.hashCode()時,null沒有hashCode()方法,也拋錯。

HashMap:

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

當key爲null時,將其hash值置爲0.

3.Enumeration

http://blog.csdn.net/zhiweianran/article/details/7672433

Hashtable實現了Enumeration

參考:

http://alex09.iteye.com/blog/539545#comments

http://www.admin10000.com/document/3322.html

http://blog.csdn.net/wisgood/article/details/16342343

關於hashCode引起的hash衝突:

http://blog.csdn.net/fenglibing/article/details/8905007

相關文章
相關標籤/搜索