美文网首页
HashMap(3.10)

HashMap(3.10)

作者: lqsss | 来源:发表于2018-03-10 09:41 被阅读0次

    源码分析

    变量解释

    约定前面的数组结构的每一个格格称为桶
    约定桶后面存放的每一个数据称为bin

    transient int size;HashMap中存放KV的数量(为链表和树中的KV的总和)
    int threshold; 如果当前size超过threshold,会调用resize方法
    (capacity * load factor)
    final float loadFactor;装载因子,默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。

        final int capacity() {
            return (table != null) ? table.length :
                (threshold > 0) ? threshold :
                DEFAULT_INITIAL_CAPACITY;
        }
    

    capacity:HashMap中桶的数量。默认值为16

    重要函数解析(JDK1.7)

    put

    public V put(K key, V value) {
          if (table == EMPTY_TABLE) {
              inflateTable(threshold);
          }
          if (key == null)
              return putForNullKey(value);
          int hash = hash(key);
          int i = indexFor(hash, table.length);
          for (Entry<K,V> e = table[i]; e != null; e = e.next) {
              Object k;
              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                  V oldValue = e.value;
                  e.value = value;
                  e.recordAccess(this);
                  return oldValue;
              }
          }
     
          modCount++;
          addEntry(hash, key, value, i);
          return null;
      }
    
      void addEntry(int hash, K key, V value, int bucketIndex) {
          if ((size >= threshold) && (null != table[bucketIndex])) {
              resize(2 * table.length);
              hash = (null != key) ? hash(key) : 0;
              bucketIndex = indexFor(hash, table.length);
          }
          createEntry(hash, key, value, bucketIndex);
      }
    

    indexFor

    static int indexFor(int h, int length) {    
         return h & (length-1);    
     }   
    

    最常见的取hash算法是value mod length,而这里用的是‘&’运算
    分析:HashMap的capacity都是2的幂,length-1转换成二进制就全是1,例如(16的容量,length-1就为1111),这样&运算的话,参与运算的另一个成员的每位是什么,运算结果就是什么。假设与hash值参与运算的某一位为0,那么不管hash的对应的那一位是什么,对应的运算结果位都为0,造成hash冲突。
    &运算的好处

    1. 1.length(2的整数次幂)的特殊性导致了length-1的特殊性(二进制全为1)
    2. 位运算快于十进制运算,hashmap扩容也是按位扩容,所以相比较就选择了后者

    put过程

    1. 首先检测table是否为空,为空则创建一个数组
    2. 判断key是否为null。若为null,特殊处理,创建一个(key为null)hash值为0的k-v对
    3. 算出key所对应的hash值,找出数组中对应的下标i
    4. 遍历table[i]是否存在相同的键值,如果相同则覆盖,返回一个老value值
    5. 如果没有一样的,调用addEntry(),判断在插入之前的size是否超过threshold,如果超过就调用resize()扩容两倍的长度,注意扩容的顺序,扩容之前old1->old2->old3,扩容之后old3->old2->old1
    6. 如果扩容了,因为数组的长度变了,需要重新计算下标。
    7. 插入:注意插入的顺序,原先table[index]的链表比如 old1->old2->old3,插入新值之后为new1->old1->old2->old3.

    1.7之前是头插法,1.8之后是尾插法。因为1.8是数组+ 链表+红黑树(table[i]超过8个),效率变高,之前头插法是因为如果链表接了10000个,尾插法遍历太多。

    resize

       void resize(int newCapacity) {    
           Entry[] oldTable = table;    
           int oldCapacity = oldTable.length;    
           if (oldCapacity == MAXIMUM_CAPACITY) {    
               threshold = Integer.MAX_VALUE;    
               return;    
           }    
        
           Entry[] newTable = new Entry[newCapacity];    
           transfer(newTable);    
           table = newTable;    
           threshold = (int)(newCapacity * loadFactor);    
       }    
    

    transfer

        void transfer(Entry[] newTable, boolean rehash) {  
            int newCapacity = newTable.length;  
            for (Entry<K,V> e : table) {  
      
                while(null != e) {  
                    Entry<K,V> next = e.next;            ---------------------(1)  
                    if (rehash) {  
                        e.hash = null == e.key ? 0 : hash(e.key);  
                    }  
                    int i = indexFor(e.hash, newCapacity);   
                    e.next = newTable[i];  
                    newTable[i] = e;  
                    e = next;  
                } // while  
            }  
    

    get

    public V get(Object key) {
           if (key == null)
               return getForNullKey();
           Entry<K,V> entry = getEntry(key);
     
           return null == entry ? null : entry.getValue();
       }
       final Entry<K,V> getEntry(Object key) {
           if (size == 0) {
               return null;
           }
     
           int hash = (key == null) ? 0 : hash(key);
           for (Entry<K,V> e = table[indexFor(hash, table.length)];
                e != null;
                e = e.next) {
               Object k;
               if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   return e;
           }
           return null;
       }
    

    get过程:

    1. 如果key为null,则在table[0]寻找
    2. 否则计算key对应的hash,找到在table表中的下标
    3. 遍历table[i]的链表,通过hash和equals找到Entry
    4. 得到entry中的value

    多线程为什么导致死循环

    1. size超过当前最大容量*负载因子时候会发生resize
    2. 两个线程同时执行了resize的方法(transfer()),在各自内部线程的栈里都生成了局部的table。
      比如:
    3. 扩容前 table[i] - >3 - > 7 - > null
    4. 需要扩容了,transfer方法:
    • 线程1: e指向3,nexr指向7 暂时挂起
    • 线程2:完成扩容,table[j] = 7 -> 3 - > null
    1. 线程1获取时间片,继续执行循环,这时把table[j] - >3 -> null,此时e指向7,next指向3;
      然后继续插入,table[j] -> 7 -> 3 - >null,但是此时7的next已经是3,不是null了,则继续循环3;
      e指向3,next为null;循环之后,3指向7;
      互相指向,以后一旦执行到遍历next操作,会死循环,cpu占用率到100%

    参考

    HashMap的indexFor方法
    Java集合框架:HashMap
    疫苗:JAVA HASHMAP的死循环

    相关文章

      网友评论

          本文标题:HashMap(3.10)

          本文链接:https://www.haomeiwen.com/subject/xqyvfftx.html