HashMap詳解

  1. HashMap的概述
  2. HashMap的數據結構及解決 hash衝突的方法
  3. HashMap源碼分析:存儲,讀取,擴容
  4. HashMap的多線程不安全的原因
  5. HashMap的多線程不安全的解決方法

HashMap的概述

HashMap 是基於哈希表的 Map 接口的非同步實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。


HashMap的數據結構及解決 hash衝突的方法

HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。從上圖中可以看出,HashMap 底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個 HashMap 的時候,就會初始化一個數組。如下圖:“
這裏寫圖片描述

transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

HashMap中主要是通過key的hashcode來計算hash值,只要hashcode的值相同,hash值就一樣。加入存儲的對象很多,那麼就有可能出現相同的hash值,也就是出現所謂的hash衝突。爲了解決hash衝突,HashMap底層應用鏈表來解決的。(JDK 1.8 以前 HashMap 的實現是 數組+鏈表,即使哈希函數取得再好,也很難達到元素百分百均勻分佈。當 HashMap 中有大量的元素都存放到同一個桶中時,這個桶下有一條長長的鏈表,這個時候 HashMap 就相當於一個單鏈表,假如單鏈表有 n 個元素,遍歷的時間複雜度就是 O(n),完全失去了它的優勢。針對這種情況,JDK 1.8 中引入了 紅黑樹(查找時間複雜度爲 O(logn))來優化這個問題。)


HashMap源碼分析:存儲,讀取,擴容

存儲:

1. public V put(K key, V value) {
2. // HashMap 允許存放 null 鍵和 null 值。
3. // 當 key 爲 null 時,調用 putForNullKey 方法,將 value 放置在數組第一個位置。
4. if (key == null)
5. return putForNullKey(value);
6. // 根據 key 的 keyCode 重新計算 hash 值。
7. int hash = hash(key.hashCode());
8. // 搜索指定 hash 值在對應 table 中的索引。
9. int i = indexFor(hash, table.length);
10. // 如果 i 索引處的 Entry 不爲 null,通過循環不斷遍歷 e 元素的下一個元素。
11. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
12. Object k;
13. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
14. V oldValue = e.value;
15. e.value = value;
16. e.recordAccess(this);
17. return oldValue;
18. }
19. }
20. // 如果 i 索引處的 Entry 爲 null,表明此處還沒有 Entry。
21. modCount++;
22. // 將 key、value 添加到 i 索引處。
23. addEntry(hash, key, value, i);
24. return null;
25. }

從上面的源代碼中可以看出:當我們往 HashMap 中 put 元素的時候,先根據 key 的
hashCode 重新計算 hash 值,根據 hash 值得到這個元素在數組中的位置(即下標),如
果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新
加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到
此數組中的該位置上。

1. void addEntry(int hash, K key, V value, int bucketIndex) {
2. // 獲取指定 bucketIndex 索引處的 Entry
3. Entry<K,V> e = table[bucketIndex];
4. // 將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entr
y
5. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
6. // 如果 Map 中的 key-value 對的數量超過了極限
7. if (size++ >= threshold)
8. // 把 table 對象的長度擴充到原來的 2 倍。
9. resize(2 * table.length);
10. }

上面方法的代碼很簡單,但其中包含了一個設計:系統總是將新添加的 Entry 對象放入 table 數組的 bucketIndex 索引處——如果 bucketIndex 索引處已經有了一個 Entry 對象,那新添加的 Entry 對象指向原有的 Entry 對象(產生一個 Entry 鏈),如果 bucketIndex 索引處沒有 Entry 對象,也就是上面程序代碼的 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是沒有產生 Entry 鏈。
HashMap裏面沒有出現hash衝突時,沒有形成單鏈表時,hashmap查找元素很快,get()方法能夠直接定位到元素,但是出現單鏈表後,單個bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 爲止——如果恰好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須循環到最後才能找到該元素。

讀取:

1. public V get(Object key) {
2. if (key == null)
3. return getForNullKey();
4. int hash = hash(key.hashCode());
5. for (Entry<K,V> e = table[indexFor(hash, table.length)];
6. e != null;
7. e = e.next) {
8. Object k;
9. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
10. return e.value;
11. }
12. return null;
13. }

有了上面存儲時的 hash 算法作爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從 HashMap 中 get 元素時,首先計算 key 的 hashCode,找到數組中對應位置的某一元素,然後通過 key 的 equals 方法在對應位置的鏈表中找到需要的元素。歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個 Entry 時,也會根據 hash算法找到其在數組中的存儲位置,再根據 equals 方法從該位置上的鏈表中取出該 Entry。

擴容:

1.  void resize(int newCapacity) {  
2.          Entry[] oldTable = table;  
3.          int oldCapacity = oldTable.length;  
4.          if (oldCapacity == MAXIMUM_CAPACITY) {  
5.              threshold = Integer.MAX_VALUE;  
6.              return;  
7.          }  
8.    
9.          Entry[] newTable = new Entry[newCapacity];  
10.         transfer(newTable);  
11.         table = newTable;  
12.         threshold = (int)(newCapacity * loadFactor);  
13.     }

當創建 HashMap 時,有一個默認的負載因子(load factor),其默認值爲 0.75,這是時間和空間成本上一種折衷:增大負載因子可以減少 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put() 方法都要用到查詢);減小負載因子會提高數據查詢的性能,但會增加 Hash 表所佔用的內存空間。


HashMap的多線程不安全的原因

大家都知道HashMap線程是不安全的,HashMap爲什麼線程不安全,多線程併發的時候在什麼情況下可能出現問題?
1.在hashmap做put操作的時候會調用到addEntry的方法。現在假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時得到現在的頭結點,然後A寫入新的頭結點之後,B也寫入新的頭結點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失。
2.刪除鍵值對時,當多個線程同時操作同一個數組位置的時候,也都會先取得現在狀態下該位置存儲的頭結點,然後各自去進行計算操作,之後再把結果寫會到該數組位置去,其實寫回的時候可能其他的線程已經就把這個位置給修改過了,就會覆蓋其他線程的修改。
3.addEntry中當加入新的鍵值對後鍵值對總數量超過門限值的時候會調用一個resize操作。這個操作會新生成一個新的容量的數組,然後對原數組的所有鍵值對重新進行計算和寫入新的數組,之後指向新生成的數組。當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操作,各自生成新的數組並rehash後賦給該map底層的數組table,結果最終只有最後一個線程生成的新數組被賦給table變量,其他線程的均會丟失。而且當某些線程已經完成賦值而其他線程剛開始的時候,就會用已經被賦值的table作爲原始數組,這樣也會有問題。


HashMap的多線程不安全的解決方法

1.Hashtable :HashTable 源碼中是使用 synchronized 來保證線程安全的
Map<String, String> hashtable = new Hashtable<>();
2.SynchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
3.ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
例子:來源與https://yemengying.com/2016/05/07/threadsafe-hashmap/

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TestHashMap
{
    public final static int THREAD_POOL_SIZE = 5;
    public static Map<String, Integer> crunchifyHashTableObject = null;
    public static Map<String, Integer> crunchifySynchronizedMapObject = null;
    public static Map<String, Integer> crunchifyConcurrentHashMapObject = null;
    public static HashMap<String, Integer> dhcHashMap = null;

    public static void main(String[] args) throws InterruptedException
    {
        // Test with Hashtable Object
        crunchifyHashTableObject = new Hashtable<>();
        crunchifyPerformTest(crunchifyHashTableObject);
        // Test with synchronizedMap Object
        crunchifySynchronizedMapObject = Collections.synchronizedMap(new HashMap<String, Integer>());
        crunchifyPerformTest(crunchifySynchronizedMapObject);
        // Test with ConcurrentHashMap Object
        crunchifyConcurrentHashMapObject = new ConcurrentHashMap<>();
        crunchifyPerformTest(crunchifyConcurrentHashMapObject);
    }

    public static void crunchifyPerformTest(final Map<String, Integer> crunchifyThreads)
            throws InterruptedException
    {
        System.out.println("Test started for: " + crunchifyThreads.getClass());
        for(int i = 0; i < 5; i++)
        {
            long startTime = System.nanoTime();
            ExecutorService crunchifyExServer = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
            for(int j = 0; j < THREAD_POOL_SIZE; j++)
            {
                crunchifyExServer.execute(new Runnable()
                {
                    @SuppressWarnings("unused")
                    @Override
                    public void run()
                    {
                        for(int i = 0; i < 500000; i++)
                        {
                            Integer crunchifyRandomNumber = (int) Math.ceil(Math.random() * 550000);
                            // Retrieve value. We are not using it anywhere
                            Integer crunchifyValue = crunchifyThreads.get(String.valueOf(crunchifyRandomNumber));
                            // Put value
                            crunchifyThreads.put(String.valueOf(crunchifyRandomNumber),
                                                 crunchifyRandomNumber);
                        }
                    }
                });
            }
            // Make sure executor stops
            crunchifyExServer.shutdown();
            // Blocks until all tasks have completed execution after a shutdown request
            crunchifyExServer.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
            long entTime = System.nanoTime();
            long totalTime = (entTime - startTime) / 1000000L;
            averageTime += totalTime;
            System.out.println("2500K entried added/retrieved in " + totalTime + " ms");
        }
        System.out.println("For " + crunchifyThreads.getClass() + " the average time is "
                + averageTime / 5 + " ms\n");
    }
}

運行結果爲:

Test started for: class java.util.Hashtable
2500K entried added/retrieved in 3371 ms
2500K entried added/retrieved in 2740 ms
2500K entried added/retrieved in 2847 ms
2500K entried added/retrieved in 2698 ms
2500K entried added/retrieved in 2683 ms
For class java.util.Hashtable the average time is 2867 ms

Test started for: class java.util.Collections$SynchronizedMap
2500K entried added/retrieved in 3265 ms
2500K entried added/retrieved in 2705 ms
2500K entried added/retrieved in 2662 ms
2500K entried added/retrieved in 2591 ms
2500K entried added/retrieved in 2680 ms
For class java.util.Collections$SynchronizedMap the average time is 2780 ms

Test started for: class java.util.concurrent.ConcurrentHashMap
2500K entried added/retrieved in 1614 ms
2500K entried added/retrieved in 925 ms
2500K entried added/retrieved in 806 ms
2500K entried added/retrieved in 834 ms
2500K entried added/retrieved in 1360 ms
For class java.util.concurrent.ConcurrentHashMap the average time is 1107 ms

然後HashMap還沒學習完,下次繼續。

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