HashMap與HashTable

1、繼承關係

2、HashMap和Hashtable的區別

1.區別:

  • 二者最主要的區別在於Hashtable是線程安全,而HashMap則非線程安全。java

  • Hashtable 是不容許鍵或值爲 null 的,HashMap 的鍵值則均可覺得 null。 Hashtable在咱們put 空值的時候會直接拋空指針異常,可是HashMap卻作了特殊處理。node

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

    由於Hashtable使用的是安全失敗機制(fail-safe),這種機制會使你這次讀到的數據不必定是最新的數據。編程

    若是你使用null值,就會使得其沒法判斷對應的key是不存在仍是爲空,由於你沒法再調用一次contain(key)來對key是否存在進行判斷,ConcurrentHashMap同理。數組

  • 實現方式不一樣:Hashtable 繼承了 Dictionary類,而 HashMap 繼承的是 AbstractMap 類。緩存

  • 初始化容量不一樣:HashMap 的初始容量爲:16,Hashtable 初始容量爲:11,二者的負載因子默認都是:0.75。安全

  • 擴容機制不一樣:當現有容量大於總容量 * 負載因子時,HashMap 擴容規則爲當前容量翻倍,Hashtable 擴容規則爲當前容量翻倍 + 1。數據結構

  • 迭代器不一樣:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。多線程

2.fail-fast和fail—safe

  • 快速失敗(fail—fast) 是java集合中的一種機制, 在用迭代器遍歷一個集合對象時,若是遍歷過程當中對集合對象的內容進行了修改(增長、刪除、修改),則會拋出Concurrent Modification Exception。併發

    • 原理: 迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。函數

      集合在被遍歷期間若是內容發生變化,就會改變modCount的值。

      每當迭代器使用hashNext()/next()遍歷下一個元素以前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;不然拋出異常,終止遍歷。

    • Tip:這裏異常的拋出條件是檢測到 modCount!=expectedmodCount 這個條件。若是集合發生變化時修改modCount值恰好又設置爲了expectedmodCount值,則異常不會拋出。

      所以,不能依賴於這個異常是否拋出而進行併發操做的編程,這個異常只建議用於檢測併發修改的bug。

    • 使用場景:ava.util包下的集合類都是快速失敗的,不能在多線程下發生併發修改(迭代過程當中被修改)算是一種安全機制吧。

  • 安全失敗(fail—safe):java.util.concurrent包下的容器都是安全失敗,能夠在多線程下併發使用,併發修改。

3、底層數據結構和存儲過程

1.hashMap

jdk1.8前數據結構是鏈表+數組。

jdk1.8以後是鏈表+數組+紅黑樹

1 初始化
HashMap<String, Integer> map = new HashMap<>();
複製代碼

在jdk8前,構造方法中建立一個長度是16的 Entry[] table 用來存儲鍵值對數據的。

在jdk8之後不是在HashMap的構造方法底層建立數組了,是在第一次調用put方法時建立的數組 Node[] table 用來存儲鍵值對數據。

2 存儲

底層採用的key的hashCode方法結合數組長度進行無符號右移(>>>)、按位異或(^)計算hash值,按位與(&)計算出索引

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//計算索引,n爲數組長度
(n-1) & hash

//除此以外,還能夠採用:平方取中法,去語數英
複製代碼

當兩個hashCode相等時,會產生哈希碰撞,若key值內容相同則替換舊的value,否則就鏈接到鏈表的後面,鏈表長度超過閾值8轉爲紅黑樹。

3 小結

說明:

1.size表示hashMap中K-V的實時數量,不是數組長度。
2.threshold(臨界值)=capacity(容量)* loadFactor(加載因子)。這個值是當前以佔據數組長的的最大值。size超過這個臨界值就會從新resize(擴容),擴容後hashMap容量是以前的2倍。

4、hashMap成員變量

4.1成員變量

1.序列化版本號
private static final long serialVersionUID = 362498820763181265L;
複製代碼
2.集合的初始化容量(必須是2的n次冪)
// 默認的初始容量是16 1 << 4 至關於 1*2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
複製代碼

問題:爲何必須是2的n次冪?若是輸入值不是2的冪好比10會怎麼樣?

存儲高效,儘可能減小碰撞,在(n-1)&hash求索引的時候更加均勻。 問題:若是傳入的容量默認不是2的冪

//對傳入容量進行右移位運算後進行或運算,一共進行5次或運算,能夠將當前數字中二進制最高位1的右邊所有變成1,最後+1返回
static final int tableSizeFor(int cap) {
    //-1的目的是使得找到的目標值大於或等於原值
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼
3.默認的負載因子,默認值是0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
複製代碼
4.集合最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 2的30次冪
複製代碼
5.當鏈表的值超過8則會轉爲紅黑樹(jdk1.8新增)
// 當桶(bucket)上的結點數大於這個值時會轉爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
複製代碼

問題:爲何Map桶中結點個數超過8才轉爲紅黑樹?

TreeNode佔用空間是普通Node的兩倍,空間和時間的權衡,同時若是爲8,log(8)=3,小於鏈表的平均8/2=4。 也就是說:選擇8由於符合泊松分佈,超過8的時候,機率已經很是小了,因此咱們選擇8這個數宇。

6.當鏈表的值小於6則會從紅黑樹轉回鏈表
// 當捅(bucket)上的結點數小於這個值,樹轉爲鏈表 
static final int UNTREEIFY_THRESHOLD = 6;
複製代碼
7.鏈表轉爲紅黑樹時的數組的大小閾值,即數組大小大於這個數字時,鏈表長度大於8纔會轉爲紅黑樹
// 捅中結構轉化爲紅黑樹對應的數組長度最小的值 
static final int MIN_TREEIFY_CAPACITY = 64;
複製代碼
8.table用來初始化數組
// 存儲元素的數組 
transient Node<K,V>[] table;
複製代碼
9.用來存放緩存(遍歷的時候使用)
// 存放具體元素的集合
transient Set<Map.Entry<K,V>> entrySet;
複製代碼
10.HashMap中存放元素的個數(重點)
// 存放元素的個數,size爲HashMap中K-V的實時數量,不是數組table的長度。
 transient int size;
複製代碼
11.用來記錄HashMap的修改次數
// 每次擴容和更改map結構的計數器
 transient int modCount;  
複製代碼
12.用來調整大小下一個容量的值計算方式爲(容量*負載因子)
// 臨界值 當實際大小(容量*負載因子)超過臨界值時,會進行擴容
int threshold;
複製代碼
13.哈希表的負載因子(重點)
// 負載因子
final float loadFactor;
複製代碼

說明:loadFactor加載因子,可表示hashMap的疏密程度,影響影響hash操做到同一個數組位置的機率,默認0.75,不建議修改。

4.2構造方法

1.構造一個空的HashMap,默認初始容量(16)和默認負載因子(0.75)
// 將默認的負載因子0.75賦值給loadFactor,並無建立數組
public HashMap() {
   this.loadFactor = DEFAULT_LOAD_FACTOR; 
}
複製代碼
2.構造一個具備指定的初始容量和默認負載因子(0.75)HashMap
// 指定「容量大小」的構造函數
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製代碼
3.構造一個具備指定的初始容量和負載因子的 HashMap
/* 指定「容量大小」和「負載因子」的構造函數 initialCapacity:指定的容量 loadFactor:指定的負載因子 */
public HashMap(int initialCapacity, float loadFactor) {
    	// 判斷初始化容量initialCapacity是否小於0
        if (initialCapacity < 0)
            // 若是小於0,則拋出非法的參數異常IllegalArgumentException
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    	// 判斷初始化容量initialCapacity是否大於集合的最大容量MAXIMUM_CAPACITY
        if (initialCapacity > MAXIMUM_CAPACITY)
            // 若是超過MAXIMUM_CAPACITY,會將MAXIMUM_CAPACITY賦值給initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
    	// 判斷負載因子loadFactor是否小於等於0或者是不是一個非數值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            // 若是知足上述其中之一,則拋出非法的參數異常IllegalArgumentException
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
     	// 將指定的負載因子賦值給HashMap成員變量的負載因子loadFactor
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
複製代碼
4.包含另外一個「Map」的構造函數
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //獲取參數集合的長度
    int s = m.size();
    if (s > 0) {
        //判斷參數集合的長度是否大於0,說明大於0
        if (table == null) { // 判斷table是否已經初始化
                // 未初始化,s爲m的實際元素個數
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
                // 計算獲得的t大於閾值,則初始化閾值
                if (t > threshold)
                    threshold = tableSizeFor(t);
        }
        // 已初始化,而且m元素個數大於閾值,進行擴容處理
        else if (s > threshold)
            resize();
        // 將m中的全部元素添加至HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}
複製代碼

4.3成員方法

1.增長方法(put)
  • 先判斷數組是否初始化,若是沒有初始化,則進行一次初始化操做(擴容),同時將數組大小賦給n
  • 先找到具體的桶,並判斷此位置是否有元素,若是沒有元素,則建立一個Node直接插入
  • 若是有元素,則出現衝突
    • 若是爲紅黑樹節點,調用紅黑樹方法插入
    • 若是爲普通節點,插入鏈表末尾,而且長度達到臨界點時,將鏈表轉爲紅黑樹
  • 若是桶中存在重複的鍵,將該鍵替換新值value
  • size大於閾值threshold,進行擴容
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}


final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    /* 1)transient Node<K,V>[] table; 表示存儲Map集合中元素的數組。 2)(tab = table) == null 表示將空的table賦值給tab,而後判斷tab是否等於null,第一次確定是null。 3)(n = tab.length) == 0 表示將數組的長度0賦值給n,而後判斷n是否等於0,n等於0,因爲if判斷使用雙或,知足一個便可,則執行代碼 n = (tab = resize()).length; 進行數組初始化,並將初始化好的數組長度賦值給n。 4)執行完n = (tab = resize()).length,數組tab每一個空間都是null。 */
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /* 1)i = (n - 1) & hash 表示計算數組的索引賦值給i,即肯定元素存放在哪一個桶中。 2)p = tab[i = (n - 1) & hash]表示獲取計算出的位置的數據賦值給結點p。 3) (p = tab[i = (n - 1) & hash]) == null 判斷結點位置是否等於null,若是爲null,則執行代碼:tab[i] = newNode(hash, key, value, null);根據鍵值對建立新的結點放入該位置的桶中。 小結:若是當前桶沒有哈希碰撞衝突,則直接把鍵值對插入空間位置。 */ 
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 建立一個新的結點存入到桶中
        tab[i] = newNode(hash, key, value, null);
    else {
         // 執行else說明tab[i]不等於null,表示這個位置已經有值了
        Node<K,V> e; K k;
        /* 比較桶中第一個元素(數組中的結點)的hash值和key是否相等 1)p.hash == hash :p.hash表示原來存在數據的hash值 hash表示後添加數據的hash值 比較兩個hash值是否相等。 說明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node對象。 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } 而在Node類中具備成員變量hash用來記錄着以前數據的hash值的。 2)(k = p.key) == key :p.key獲取原來數據的key賦值給k key 表示後添加數據的key比較兩個key的地址值是否相等。 3)key != null && key.equals(k):可以執行到這裏說明兩個key的地址值不相等,那麼先判斷後添加的key是否等於null,若是不等於null再調用equals方法判斷兩個key的內容是否相等。 */
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                /* 說明:兩個元素哈希值相等,而且key的值也相等,將舊的元素總體對象賦值給e,用e來記錄 */ 
                e = p;
        // hash值不相等或者key不相等;判斷p是否爲紅黑樹結點
        else if (p instanceof TreeNode)
            // 放入樹中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 說明是鏈表結點
        else {
            /* 1)若是是鏈表的話須要遍歷到最後結點而後插入 2)採用循環遍歷的方式,判斷鏈表中是否有重複的key */
            for (int binCount = 0; ; ++binCount) {
                /* 1)e = p.next 獲取p的下一個元素賦值給e。 2)(e = p.next) == null 判斷p.next是否等於null,等於null,說明p沒有下一個元素,那麼此時到達了鏈表的尾部,尚未找到重複的key,則說明HashMap沒有包含該鍵,將該鍵值對插入鏈表中。 */
                if ((e = p.next) == null) {
                    /* 1)建立一個新的結點插入到尾部 p.next = newNode(hash, key, value, null); Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); } 注意第四個參數next是null,由於當前元素插入到鏈表末尾了,那麼下一個結點確定是null。 2)這種添加方式也知足鏈表數據結構的特色,每次向後添加新的元素。 */
                    p.next = newNode(hash, key, value, null);
                    /* 1)結點添加完成以後判斷此時結點個數是否大於TREEIFY_THRESHOLD臨界值8,若是大於則將鏈表轉換爲紅黑樹。 2)int binCount = 0 :表示for循環的初始化值。從0開始計數。記錄着遍歷結點的個數。值是0表示第一個結點,1表示第二個結點。。。。7表示第八個結點,加上數組中的的一個元素,元素個數是9。 TREEIFY_THRESHOLD - 1 --》8 - 1 ---》7 若是binCount的值是7(加上數組中的的一個元素,元素個數是9) TREEIFY_THRESHOLD - 1也是7,此時轉換紅黑樹。 */
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 轉換爲紅黑樹
                        treeifyBin(tab, hash);
                    // 跳出循環
                    break;
                }
                 
                /* 執行到這裏說明e = p.next 不是null,不是最後一個元素。繼續判斷鏈表中結點的key值與插入的元素的key值是否相等。 */
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循環
                    /* 要添加的元素和鏈表中的存在的元素的key相等了,則跳出for循環。不用再繼續比較了 直接執行下面的if語句去替換去 if (e != null) */
                    break;
                /* 說明新添加的元素和當前結點不相等,繼續查找下一個結點。 用於遍歷桶中的鏈表,與前面的e = p.next組合,能夠遍歷鏈表 */
                p = e;
            }
        }
        /* 表示在桶中找到key值、hash值與插入元素相等的結點 也就是說經過上面的操做找到了重複的鍵,因此這裏就是把該鍵的值變爲新的值,並返回舊值 這裏完成了put方法的修改功能 */
        if (e != null) { 
            // 記錄e的value
            V oldValue = e.value;
            // onlyIfAbsent爲false或者舊值爲null
            if (!onlyIfAbsent || oldValue == null)
                // 用新值替換舊值
                // e.value 表示舊值 value表示新值 
                e.value = value;
            // 訪問後回調
            afterNodeAccess(e);
            // 返回舊值
            return oldValue;
        }
    }
    // 修改記錄次數
    ++modCount;
    // 判斷實際大小是否大於threshold閾值,若是超過則擴容
    if (++size > threshold)
        resize();
    // 插入後回調
    afterNodeInsertion(evict);
    return null;
}
複製代碼
2.鏈表轉紅黑樹(treeifyBin)
/* 替換指定哈希表的索引處桶中的全部連接結點,除非表過小,不然將修改大小。 Node<K,V>[] tab = tab 數組名 int hash = hash表示哈希值 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    /* 若是當前數組爲空或者數組的長度小於進行樹形化的閾值(MIN_TREEIFY_CAPACITY = 64),就去擴容。而不是將結點變爲紅黑樹。 目的:若是數組很小,那麼轉換紅黑樹,而後遍歷效率要低一些。這時進行擴容,那麼從新計算哈希值,鏈表長度有可能就變短了,數據會放到數組中,這樣相對來講效率高一些。 */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //擴容方法
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        /* 1)執行到這裏說明哈希表中的數組長度大於閾值64,開始進行樹形化 2)e = tab[index = (n - 1) & hash]表示將數組中的元素取出賦值給e,e是哈希表中指定位置桶裏的鏈表結點,從第一個開始 */
        // hd:紅黑樹的頭結點 tl:紅黑樹的尾結點
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 新建立一個樹的結點,內容和當前鏈表結點e一致
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p; // 將新創鍵的p結點賦值給紅黑樹的頭結點
            else {
                p.prev = tl; // 將上一個結點p賦值給如今的p的前一個結點
                tl.next = p; // 將如今結點p做爲樹的尾結點的下一個結點
            }
            tl = p;
            /* e = e.next 將當前結點的下一個結點賦值給e,若是下一個結點不等於null 則回到上面繼續取出鏈表中結點轉換爲紅黑樹 */
        } while ((e = e.next) != null);
        /* 讓桶中的第一個元素即數組中的元素指向新建的紅黑樹的結點,之後這個桶裏的元素就是紅黑樹 而不是鏈表數據結構了 */
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
複製代碼
3. 擴容方法 resize()
final Node<K,V>[] resize() {
    // 獲得當前數組
    Node<K,V>[] oldTab = table;
    // 若是當前數組等於null長度返回0,不然返回當前數組的長度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //當前閥值點 默認是12(16*0.75)
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 若是老的數組長度大於0
    // 開始計算擴容後的大小
    if (oldCap > 0) {
        // 超過最大值就再也不擴充了,就只好隨你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 修改閾值爲int的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        /* 沒超過最大值,就擴充爲原來的2倍 1) (newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍以後容量要小於最大容量 2)oldCap >= DEFAULT_INITIAL_CAPACITY 原數組長度大於等於數組初始化長度16 */
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 閾值擴大一倍
            newThr = oldThr << 1; // double threshold
    }
    // 老閾值點大於0 直接賦值
    else if (oldThr > 0) // 老閾值賦值給新的數組長度
        newCap = oldThr;
    else { // 直接使用默認值
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 計算新的resize最大上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 新的閥值 默認原來是12 乘以2以後變爲24
    threshold = newThr;
    // 建立新的哈希表
    @SuppressWarnings({"rawtypes","unchecked"})
    //newCap是新的數組長度--》32
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 判斷舊數組是否等於空
    if (oldTab != null) {
        // 把每一個bucket都移動到新的buckets中
        // 遍歷舊的哈希表的每一個桶,從新計算桶裏元素的新位置
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 原來的數據賦值爲null 便於GC回收
                oldTab[j] = null;
                // 判斷數組是否有下一個引用
                if (e.next == null)
                    // 沒有下一個引用,說明不是鏈表,當前桶上只有一個鍵值對,直接插入
                    newTab[e.hash & (newCap - 1)] = e;
                //判斷是不是紅黑樹
                else if (e instanceof TreeNode)
                    // 說明是紅黑樹來處理衝突的,則調用相關方法把樹分開
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 採用鏈表處理衝突
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 經過上述講解的原理來計算結點的新位置
                    do {
                        // 原索引
                        next = e.next;
                     	// 這裏來判斷若是等於true e這個結點在resize以後不須要移動位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket裏
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket裏
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
複製代碼
4.刪除方法remove()
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
	// 根據hash找到位置 
	// 若是當前key映射到的桶不爲空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 若是桶上的結點就是要找的key,則將node指向該結點
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 說明結點存在下一個結點
            if (p instanceof TreeNode)
                // 說明是以紅黑樹來處理的衝突,則獲取紅黑樹要刪除的結點
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 判斷是否以鏈表方式處理hash衝突,是的話則經過遍歷鏈表來尋找要刪除的結點
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 比較找到的key的value和要刪除的是否匹配
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 經過調用紅黑樹的方法來刪除結點
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 鏈表刪除
                tab[index] = node.next;
            else
                p.next = node.next;
            // 記錄修改次數
            ++modCount;
            // 變更的數量
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
複製代碼
5.查找元素方法get()
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 若是哈希表不爲空而且key對應的桶上不爲空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        /* 判斷數組元素是否相等 根據索引的位置檢查第一個元素 注意:老是檢查第一個元素 */
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 若是不是第一個元素,判斷是否有後續結點
        if ((e = first.next) != null) {
            // 判斷是不是紅黑樹,是的話調用紅黑樹中的getTreeNode方法獲取結點
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 不是紅黑樹的話,那就是鏈表結構了,經過循環的方法判斷鏈表中是否存在該key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
複製代碼

5、如何保證線程安全

1.使用Collections.synchronizedMap(Map)建立線程安全的map集合

在SynchronizedMap內部維護了一個普通對象Map,還有排斥鎖mutex。

若是沒有,則將對象排斥鎖賦值爲this,即調用synchronizedMap的對象,就是上面的Map。

建立出synchronizedMap以後,再操做map的時候,就會對方法上鎖

2. ConcurrentHashMap

1.底層數據結構(jdk1.7)

如圖所示,是由 Segment 數組、HashEntry 組成,和 HashMap 同樣,仍然是 數組加鏈表

Segment 是 ConcurrentHashMap 的一個內部類,主要的組成以下:

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

    private static final long serialVersionUID = 2249069246763182397L;

    // 和 HashMap 中的 HashEntry 做用同樣,真正存放數據的桶
    transient volatile HashEntry<K,V>[] table;

    transient int count;
        // 記得快速失敗(fail—fast)麼?
    transient int modCount;
        // 大小
    transient int threshold;
        // 負載因子
    final float loadFactor;
}
複製代碼

HashEntry跟HashMap差很少的,可是不一樣點是,他使用volatile去修飾了他的數據Value還有下一個節點next。

2.併發度高的緣由(jdk1.7)

原理上來講,ConcurrentHashMap 採用了分段鎖技術,其中 Segment 繼承於 ReentrantLock。

不會像 HashTable 那樣無論是 put 仍是 get 操做都須要作同步處理,理論上 ConcurrentHashMap 支持 CurrencyLevel (Segment 數組數量)的線程併發。

每當一個線程佔用鎖訪問一個 Segment 時,不會影響到其餘的 Segment。

就是說若是容量大小是16他的併發度就是16,能夠同時容許16個線程操做16個Segment並且仍是線程安全的。

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();//這就是爲啥他不能夠put null值的緣由
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          
         (segments, (j << SSHIFT) + SBASE)) == null) 
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}
複製代碼

先定位到Segment,而後再進行put操做。

//put源碼 
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
          // 將當前 Segment 中的 table 經過 key 的 hashcode 定位到 HashEntry
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
 // 遍歷該 HashEntry,若是不爲空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value。
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                 // 不爲空則須要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否須要擴容。
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
               //釋放鎖
                unlock();
            }
            return oldValue;
        }
複製代碼

首先第一步的時候會嘗試獲取鎖,若是獲取失敗確定就有其餘線程存在競爭,則利用scanAndLockForPut() 自旋獲取鎖。

  1. 嘗試自旋獲取鎖。
  2. 若是重試的次數達到了 MAX_SCAN_RETRIES 則改成阻塞鎖獲取,保證能獲取成功。

get邏輯

get 邏輯比較簡單,只須要將 Key 經過 Hash 以後定位到具體的 Segment ,再經過一次 Hash 定位到具體的元素上。

因爲 HashEntry 中的 value 屬性是用 volatile 關鍵詞修飾的,保證了內存可見性,因此每次獲取時都是最新值。

ConcurrentHashMap 的 get 方法是很是高效的,由於整個過程都不須要加鎖

1.3 底層數據結構(jdk1.8)

其中拋棄了原有的 Segment 分段鎖,而採用了 CAS + synchronized 來保證併發安全性。

跟HashMap很像,也把以前的HashEntry改爲了Node,可是做用不變,把值和next採用了volatile去修飾,保證了可見性,而且也引入了紅黑樹,在鏈表大於必定值的時候會轉換(默認是8)。

1.4 存取操做?以及是怎麼保證線程安全的?(jdk1.8)
  • put操做步驟:
  1. 根據 key 計算出 hashcode 。

  2. 判斷是否須要進行初始化。

  3. 即爲當前 key 定位出的 Node,若是爲空表示當前位置能夠寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功。

  4. 若是當前位置的 hashcode == MOVED == -1,則須要進行擴容。

  5. 若是都不知足,則利用 synchronized 鎖寫入數據。

  6. 若是數量大於 TREEIFY_THRESHOLD 則要轉換爲紅黑樹。

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //計算出hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //判斷是否須要初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //爲當前key定位出的node,若是爲空表示當前位置能夠寫入數據,利用CAS嘗試寫入,失敗則自選保證成功。
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //若是當前位置的 hashcode == MOVED == -1,則須要進行擴容。
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
            //若是都不知足,則利用 synchronized 鎖寫入數據。
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                //若是數量大於 `TREEIFY_THRESHOLD` 則要轉換爲紅黑樹。
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
複製代碼

+的get操做步驟:

1.根據計算出來的 hashcode 尋址,若是就在桶上那麼直接返回值。

2.若是是紅黑樹那就按照樹的方式獲取值。

3.就不知足那就按照鏈表的方式遍歷獲取值。

1.5 CAS是什麼?自旋又是什麼?

CAS 是樂觀鎖的一種實現方式,是一種輕量級鎖,JUC 中不少工具類的實現就是基於 CAS 的。

CAS 操做的流程以下圖所示,線程在讀取數據時不進行加鎖,在準備寫回數據時,比較原值是否修改,若未被其餘線程修改則寫回,若已被修改,則從新執行讀取流程。

這是一種樂觀策略,認爲併發操做並不總會發生。

1.6 CAS性能很高,可是我知道synchronized性能可不咋地,爲啥jdk1.8升級以後反而多了synchronized?

synchronized以前一直都是重量級的鎖,可是後來java官方是對他進行過升級的,他如今採用的是鎖升級的方式去作的。

針對 synchronized 獲取鎖的方式,JVM 使用了鎖升級的優化方式,就是先使用偏向鎖優先同一線程而後再次獲取鎖,若是失敗,就升級爲 CAS 輕量級鎖,若是失敗就會短暫自旋,防止線程被系統掛起。最後若是以上都失敗就升級爲重量級鎖

因此是一步步升級上去的,最初也是經過不少輕量級的方式鎖定的。

相關文章
相關標籤/搜索