HashMap、HashTable、ConcurrentHashMap一些小總結

一、 前言

本文基本是自己看完之後的一個總結記錄,所以寫的很混亂,很多語言的描述也並不清晰。
推薦 : https://blog.csdn.net/u012403290/article/details/68488562 講的比我要清晰多了。本文只作爲個人記錄使用。

二、HashMap

1. Node

HashMap 底層實現是通過一個內部類數組 transient Node<K,V>[] table;
這裏Node是個自定義內部類如下,可以看出來Node 的本質是一個單向鏈表。

 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;
        }
        ... 其它代碼
}

四個屬性意義分別如下:
hash : 保存key的hash值
key: 保存節點的key值
value: 保存節點的value值
next: 指向下一個Node節點

HashMap結構如圖(手工畫,略醜)

在這裏插入圖片描述
爲了方便描述,我們將數組上的每一個元素和所鏈接的元素鏈表或樹稱爲桶。如Node A0、Node A1、Node A2 這樣一個結構稱爲桶。將NodeA0、A1稱爲桶的節點

2、put 方法

註釋寫的比較詳細,寫了很多次都沒寫出來一個好點的例子。

    // 保存數據的的Node數組
   transient Node<K,V>[] table;

  /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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. 如果 Node 數組還沒初始化,則進行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
		// 2. 如果key的hash處理後所對應的table數組位置的桶還沒有初始化(table[i] = null, 說明table的第i個位置還沒有Node節點,所以說桶還沒有初始化),則創建新節點並插入,作爲當前位置桶的第一個節點
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
			// 3. 判斷如果key值等於當前桶節點的key值,則記錄下節點(e = p)。留待後面處理
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
			// 4. 如果當前桶已經是轉化爲紅黑樹結構,則以紅黑樹規則插入節點
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
			// 5. 到這裏說明, 當前位置存在桶結構(即存在Node節點),且當前位置尚不構成紅黑樹結構
                for (int binCount = 0; ; ++binCount) {
					// 6. 如果p節點就是最後一個節點(p.next = null), 就初始化e節點,並添加在p節點後(因爲p 節點已經是最後一個節點,所以當前桶中沒有當前節點,新建節點,插入末尾)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
						// 7. 如果當前桶節點數量大於等於7。則轉換成紅黑樹結構。小於等於6時恢復成鏈表(爲了保證查找效率,在連接結構大於等於7的情況下會轉換爲樹結構)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					// 8. 如果找到了匹配了當前key的hash的節點。跳出循環,進行value賦值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
					// 9. 將e 賦值給 p (進行下一個節點的判斷)
                    p = e;
                }
            }
			// 10. 如果 e 不爲空,則說明找到了對應key的桶元素
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
				// 11. 進行新的value賦值,並返回舊value值  -  onlyIfAbsent 在put 方法中恆定傳false
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
		// 12. 記錄map修改次數,在快速失敗時使用
        ++modCount;
		// 13. 計算如果新增後的大小超過閾值,則重新設置大小
        if (++size > threshold)
            resize();
		// 14. 進行插入的後操作,供子類實現
        afterNodeInsertion(evict);
        return null;
    }
	
	//  在 remove -> removeNode -> removeTreeNode 方法中判斷是否解除樹化

3、get 方法

get 方法相較於put方法更加簡單

  /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
		// 如果table不爲空,且對應的桶不爲空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
			// 如果 找到對應keyHash的值,則返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
			// 如果下面一個節點不爲空,則 讓 
            if ((e = first.next) != null) {
				// 如果是紅黑樹,則按照紅黑樹的邏輯查找節點
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
				// 否則桶一直往下遍歷
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

4、entrySet 方法的遍歷

  1. 在HashMap 中有一種遍歷方式如下

            HashMap<String, String> hashMap = new HashMap();
            hashMap.put("A", "1");
            hashMap.put("B", "1");
            Set<Map.Entry<String, String>> set = hashMap.entrySet();
            for (Map.Entry<String, String> entry : set) {
                System.out.println(entry.getKey());
                System.out.println(entry.getValue());
            }
    

    這是一種很常見的遍歷方式,我們點進去entrySet方法,看到如下。
    我們知道HashMap 中所有的數據都存放在Node[] 數組中,那麼這個 entrySet是如何實現遍歷整個Map的呢?
    可以看到entrySet方法中初始化了 entrySet 變量。我們進入EntrySet類中發現並無其他。

        transient Set<Map.Entry<K,V>> entrySet;
        
        public Set<Map.Entry<K,V>> entrySet() {
            Set<Map.Entry<K,V>> es;
            return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
        }
    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }
    
  2. 這時需要注意的是: **forEach 只是一種語法糖,其底層是通過迭代器實現的。在反編譯後的代碼其實是迭代器實現。**所以我們的遍歷代碼在編譯後其實是下面這種形式。可以看到他調用的是 iterator() 方法。

   Iterator<Map.Entry<String, String>> iterator = set.iterator();
        while (iterator.hasNext()){
            Map.Entry<String, String> next = iterator.next();
            System.out.println(next.getKey());
            System.out.println(next.getValue());
        }
  1. 所以我們進入EntrySet.iterator()方法中,EntrySet.iterator()中只初始化了一個 EntryIterator() ,這也是個HashMap 內部類。再進去EntryIterator類中,發現EntryIterator 類繼承了HashIterator 類,再進去 HashIterator 類中。 所以整個過程是 EntrySet -> EntryIterator -> HashIterator。 這幾個類都是HashMap 內部類。
    EntryIterator 源碼如下 ,下面可以看到,next方式調用的是父類的nextNode 方法,即HashIterator.nextNode 方法
    final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
  1. HashIterator 代碼如下,我們可以就豁然開朗了,註釋都在代碼中。
    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            // 初始化的時候將table賦值給t 
            Node<K,V>[] t = table;
            current = next = null;
            // 設置順序從0開始
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }
		// 當我們調用next 方法時,就會調用這個方法。這個方法的作用就是將獲取下一個節點並返回。
        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

二、HashTable 實現線程安全

HashTable 實現 線程安全主要是通過加鎖(synchronized)。通過鎖住整個數組結構來保證線程安全,除此之外,實現基本和HashMap 相同,需要注意的是,HashTable中沒有使用紅黑樹結構,全部使用鏈表結構

1、 Entry 類

這裏的Entry 和 HashMap 中的Node 相同,就是換了個名字,換湯不換藥。
在這裏插入圖片描述

2、 put 方法

這個比HashMap 還簡單

public synchronized V put(K key, V value) {
        // 非空判斷
        if (value == null) {
            throw new NullPointerException();
        }

        // 確保key值還未保存在 table 數組中
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        // 遍歷查找,如果找到對應的key值,則將value替換
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
		// 否則添加新的節點
        addEntry(hash, key, value, index);
        return null;
    }

3、 get 方法

get 方法更加簡單。

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 找到下標
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 找到對應桶,遍歷節點
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

三、ConcurrentHashMap

HashMap 由於加了全局鎖,會導致併發情況下效率低下,相比較而言 ConcurrentHashMap 效率要高得多。相較而言ConcurrentHashMap 也可以保證線程安全。ConcurrentHashMap 的思想是分段鎖,而不是像HashTable一樣的全局鎖。 在Jdk 1.7 和 Jdk1.8中 ConcurrentHashMap 的實現是不同的。這裏主要介紹Jdk1.8。所以只是簡單提一下Jdk1.7。

在Jdk1.7 中 :ConcurrentHashMap 引入片段(Segment)概念, 每若干個桶都有一個片段鎖,各個片段鎖不衝突。獲取數據時先獲取當前桶的所屬片段的片段鎖。
在Jdk 1.8 中:ConcurrentHashMap 對分段鎖的更細緻的劃分,每個桶都有一個獨立的鎖。不再使用segment,使用了 CAS 來實現單獨的桶鎖。synchronized 實現單獨的桶鎖。核心思想是CAS。

1. CAS(Compare And Swap)

1.1. 概念

CAS 即 比較並交換。他是一條CPU併發原語。功能是判斷內存某個位置上的值是否爲預期值,如果是則更改爲新的值,這個過程是原子的。CAS併發原語體現在JAVA語言中就是sun.misc.Unsafe類中的各個方法。調用UnSafe類中的CAS方法, JVM會幫我們實現CAS 彙編指令。這是一種完全依賴於硬件的功能,通過它實現了原子操作。再次強調,由於CAS是一種系統原語,原語屬於操作系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。

1.2 核心類 UnSafe

UnSafe是CAS的核心類,由於Java方法無法直接訪問底層系統,需要通過本地(native) 方法來訪問,Unsafe相當於一 一個後門,基於該類可以直接操作特定內存的數據。Unsafe類存在於sun.misc包中, 其內部方法操作可以像C的指針一樣直接操作內存,因爲Java中CAS操作的執行依賴於Unsafe類的方法。

2. ConcurrentHashMap

2.1 ConcurrentHashMap 3個原子性操作方法。

	// 根據Volatile特性, 獲取到最新的table數組的第i個node。
	static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
	    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
	}
	//  四個參數分別是: 操作對象,偏移量,期待值,新值。
	// 利用CAS實現如下操作: 取出 tab數組的第i個Node元素,比較是否和c相等,相等則將c替換成V。這個操作線程安全
	static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
	                                    Node<K,V> c, Node<K,V> v) {
	    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
	}
	// 根據Volatile特性, 設置tab數組的第i個node,立即可見。
	static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
	    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
	}

2.2 put 方法

    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
		// 非空校驗
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
			// 初始化Node 數組
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
			// 獲取 Node數組某元素,如果爲空,則創建新節點插入
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
				// 通過CAS進行賦值新節點並插入Node數組中
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
			// 插入的時候如果數組正在擴容,則當前線程進行幫助擴容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
				// 鎖住某一個桶的頭結點進行操作  --> 只是鎖住了一個頭結點。HashTable 鎖住了整個Node[],效率可想而知
                synchronized (f) {
					// 如果新插入節點屬於f桶,則進入f桶中查找合適節點
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
								// 查找到key相同的節點,替換value值
                                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) {
                	// 這裏是大於等於8進行樹形轉換,小於等於6切換回鏈表
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

2.3 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());
        // 如果對應桶不爲空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            // 如果e節點hash值和key相同,則返回value
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 小於0就說明已經再擴容或者已經在初始化
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
           // 如果還沒達到桶末尾,則往下繼續查找
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

三、總結

  1. HashMapConcurrentHashMap 存儲結構是 數組+鏈表和紅黑樹 來存儲數據,HashTable 使用數組+鏈表 來存儲數據。
  2. HashTable 通過 synchronized 在某些方法上加鎖來實現線程安全。同時也使得效率變低
  3. ConcurrentHashMap 實現線程安全的原理是 CAS。通過 synchronized 來鎖住桶的第一個節點(鎖住第一個節點後其餘線程也就無法訪問這個桶了)來實現線程安全。鎖的顆粒度更細,所以效率更高。
  4. 在JDK1.7 中 使用了片段(segment)來加鎖, 一個片段鎖住若干個桶,相較於HashTable鎖的顆粒度更細,但是在JDK1.8中捨棄了segment,通過CASsynchronized 爲每個桶都加了一個鎖,顆粒度更高,效率也更高。
  5. HashMap 中,鏈表長度大於等於7時會轉換爲樹結構,小於等於6時會轉換爲鏈表;在ConcurrentHashMap 中是大於等於8時轉換爲樹結構,小於等於6時轉化爲鏈表;在HashTable中沒有使用紅黑樹的結構。

以上:內容部分自己總結
如有侵擾,聯繫刪除。 內容僅用於自我記錄學習使用。如有錯誤,歡迎指正

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章