1. 程式人生 > 其它 >什麼是邊緣計算?啟揚智慧系列智慧邊緣計算閘道器

什麼是邊緣計算?啟揚智慧系列智慧邊緣計算閘道器

Java原始碼解析之HashMap

一、HashMap原始碼解析

1、HashMap的資料結構

  • jdk7以前:陣列+連結串列
  • jdk8以後:陣列+連結串列+紅黑樹
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        ...
        ...
    }
    
    transient Node<K,V>[] table;
    
}

以上程式碼是jdk中HashMap中的部分程式碼


HashMap類中定義了一個名為Node的靜態內部類它實現了Map.Entry介面,這個內部類例項化後就是一個Entry的實現類物件,每個

Entry物件中有指向下一個元素的Node<K,V> next屬性,這就是所謂的連結串列的實現方式(通過地址指引)


HashMap類中還定義了一個Node<K,V>[] table的陣列,用於儲存一個個的Node物件


從這裡可以看出HashMap中儲存的起始是一個個的Entry物件

小知識點

序列化:一個類實現了Serializable,就相當於告訴JVM這個類是可以被序列化的,就是可以把物件通過流寫出

transient關鍵字:在一個可序列化的類中,transient關鍵字修飾的成員表示該成員不參與序列化,也就是不能寫出儲存,相當 於是一個臨時資料

反序列化:就是把序列化寫出的物件讀到程式記憶體中

下面是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;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

2、HashMap的構造方法

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
    
    static final int MAXIMUM_CAPACITY = 1 << 30;
    
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    transient int modCount; //記錄這個HashMap結構被修改的次數
    
    transient int size;	//這個HashMap中包含鍵值對的數量
    
    int threshold;	//下一次擴容的條件數值,當陣列內容達到這個值時進行擴容
}

HashMap類存在這三種常量屬性,和雜湊表的效能有關,分別是

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:定義了Node<K,V>[] table陣列的預設大小

static final int MAXIMUM_CAPACITY = 1 << 30:定義了Node<K,V>[] table陣列的最大長度

static final float DEFAULT_LOAD_FACTOR = 0.75f:定義了雜湊表的載入因子

1 << 4 << 是移位操作,是基於二進位制的操作

​ 1的二進位制 0000 0001 就是十進位制的1

​ 1 << 4 後 0001 0000 就是十進位制的16

載入因子:載入因子就是判斷陣列什麼時候進行擴容,當陣列內元素個數滿足 原陣列長度*載入因子 個時進行擴容

  • 空構造public HashMap()

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    

    呼叫空建構函式時,生成物件的欄位屬性使用的都是預設值

    陣列預設長度16bit、載入因子0.75

  • 包含陣列長度和載入因子的建構函式public HashMap(int initialCapacity, float loadFactor)

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    

    這個構造方法中,傳了一個數組的長度和載入因子的陣列作為引數,函式內就是判斷這兩個值是否符合條件,符合的話就為物件的loadFactor屬性賦值,並使用 this.threshold = tableSizeFor(initialCapacity);為threshold 賦值;否則報錯

    tableSizeFor這個函式返回一個大於指定值得最小2次冪

    使用 this.threshold = tableSizeFor(initialCapacity)為threshold 賦值???

    threshold的值不應該是 this.threshold = tableSizeFor(initialCapacity)*loadFactor嗎?

    在HashMap的建構函式,並沒有為Node<K,V>[] table進行初始化,而是把初始化的過程放在了put方法裡

  • 包含陣列長度的建構函式public HashMap(int initialCapacity)

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    

    這個就是呼叫public HashMap(int initialCapacity, float loadFactor)構造,把載入因子為預設值

3、HashMap的存值(put方法)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

呼叫put方法時,put方法會把key的hash值計算出來,然後呼叫putVal方法,返回值為Value的值

  • 1.putVal方法會先判斷陣列是否需要擴容

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    

    如果陣列為空或者長度為零,則呼叫resize方法擴容陣列,這裡才對陣列進行初始化操作

  • 2.然後計算出元素的索引,判斷陣列上該位置是否已經存過元素

    	if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
    

    如果該位置為空,則直接存入該位置

    (n-1) & hash 是求該hash值在陣列中的索引的操作

    n ---> 陣列長度

    例如:陣列長度為16 hash值為一個隨機值

    n 0001 0000 16

    n-1 0000 1111 15

    hash 1011 0011 隨機


    & 0000 0011 &後的結果 3

    (n-1) & hash相當於拿出一個隨機值的後幾位

  • 3.如果陣列當前索引處已經存有元素了,把陣列當前索引存的物件稱為 p

    • 判斷這個元素的key值是否和p的key值相同

          else {
              Node<K,V> e; K k;
              if (p.hash == hash &&
                  ((k = p.key) == key || (key != null && key.equals(k))))
                  e = p;
      

      如果相同則取出p物件,否則

    • 判斷p是否為一個樹節點

      	else if (p instanceof TreeNode)
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
      

      如果p是一個樹節點,呼叫putTreeVal方法

    • 遍歷陣列這個索引下的節點

      • 如果存在和要存元素key值相同的元素

        如果存在,則把這個元素記錄到e上,用於後續判斷

      • 如果不存在,則把新元素插入到連結串列的尾部

       	else {
              for (int binCount = 0; ; ++binCount) {
                  if ((e = p.next) == null) {
                      p.next = newNode(hash, key, value, null);
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          treeifyBin(tab, hash);
                      break;
                  }
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      break;
                  p = e;
              }
          }
      
    • 如果執行完上面的程式碼取出的元素不為空,則說明存在和要存物件key值相同的元素

      	if (e != null) { // existing mapping for key
              V oldValue = e.value;
              if (!onlyIfAbsent || oldValue == null)
                  e.value = value;
              afterNodeAccess(e);
              return oldValue;
          }
      

      用新的value值替換舊的value值,返回舊的value值

    • 最後說明插入成功,將陣列的元素數量加一,判斷是否需要擴容

          ++modCount;
          if (++size > threshold)
              resize();
          afterNodeInsertion(evict);
          return null;
      }
      

這就是put方法的全過程,此外,put方法結尾還提供了一個afterNodeAccess(e)方法,這是一個空方法,沒有任何實現,它允許我們插入元素後進行一些操作

4、HashMap的取值(get方法)

​ get方法返回一個value值,原始碼如下

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

​ get方法通過呼叫getNode方法來找到相應的鍵值對物件,下面是getNode方法的原始碼

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    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) {
            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;
}

5、HashMaps陣列擴容(resize方法)

​ 因為在HashMap的構造方法裡,並沒有對陣列進行初始化,而是在put方法裡呼叫resize方法進行對陣列的統一處理

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

先在resize方法裡定義了一些變數來表示陣列的一些資訊

  • 如果陣列已經初始化過,分為兩種情況

    • 陣列長度已經大於設定的最大容量了

      這時,把擴容條件閾值設為整數的最大值,這樣在put方法插入元素後的判斷if (++size > threshold)就始終為false(因為size是int型別,不會大於Integer.MAX_VALUE),這樣陣列就不會再進行擴容

    • 陣列長度介於預設值和最大值之間

      這時,陣列將進行正常的擴容操作,陣列長度和擴容條件閾值都變為原來的兩倍

    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    
  • 如果陣列未初始化,且條件閾值大於零

    什麼時候才會發生這種情況呢?

    當我們呼叫HashMap的非空構造方法時,在構造方法裡並沒有對陣列進行初始化,而是計算出來擴容閾值threshold的值,這樣就會出現陣列長度為零,擴容條件閾值存在的情況

    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    

    這種情況下,直接把擴容閾值作為陣列的長度進行初始化

  • 如果陣列未初始化且條件閾值不存在

    不存在的意思是沒有賦過值,這時threshold的值是預設值0

    這種情況最常見,一般是呼叫了HashMap的空構造

    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    

    這種情況下,陣列長度和條件閾值都用預設值即可、

  • if (newThr == 0)?為什麼還要有這個判斷?

    在執行else if (oldThr > 0) newCap = oldThr;這句後,也就是當呼叫HashMap的有參構造時,這裡並沒有為newThr賦值,因此newThr為預設值0

    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    

    這個判斷就是為newThr賦值

    然後把擴容後的資訊更新到物件中

    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
  • 擴容完畢,將原來的資料轉移到新陣列中

    對這段程式碼先不做解釋,諒解

二、HashMap執行緒不安全的原因

HashMap在java7以前,存在的執行緒安全問題有死迴圈、資料丟失、資料覆蓋;在java8以後,HashMap存在的執行緒安全問題是資料覆蓋

  • 多執行緒問題1-->資料覆蓋

    前提:多個執行緒進行put存值時,多個執行緒的要存的索引位置相同,並且滿足儲存條件(不存在key值相同)

    執行緒一判斷完滿足儲存條件後掛起 -----> 執行緒二啟動,並在線上程一要存的位置存值 ---> 執行緒一繼續執行,在相應位置存值

    這時就把執行緒二所存的值覆蓋掉了

    此外,在if (++size > threshold)這句程式碼裡,++size明顯是一個執行緒不安全的操作

  • 多執行緒問題2-->死迴圈

    以下是jdk7以前的HashMap陣列擴容後資料遷移的原始碼

    //資料遷移的方法,頭插法新增元素
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //for迴圈中的程式碼,逐個遍歷連結串列,重新計算索引位置,將老陣列資料複製到新陣列中去(陣列不儲存實際資料,所以僅僅是拷貝引用		//而已)
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                //將當前entry的next鏈指向新的索引位置,newTable[i]有可能為空,有可能也是個entry鏈,如果是entry鏈,直接在鏈				//表頭部插入。
                //以下三行是執行緒不安全的關鍵
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
    

    會發生執行緒安全問題的程式碼主要是e.next = newTable[i]; newTable[i] = e;這兩行

    .

  • 多執行緒問題2-->資料丟失

後面會更新,敬請見諒!