zoukankan      html  css  js  c++  java
  • hashmap 1.7

    HashMap是基于哈希表(散列表),实现Map接口的双列集合,数据结构是“链表散列”,也就是数组+链表 ,key唯一的value可以重复,允许存储null 键null 值,元素无序。

    数组:一段连续控件存储数据,指定下标的查找,时间复杂度O(1),通过给定值查找,需要遍历数组,自已对比复杂度为O(n) 二分查找插值查找,复杂度为O(logn) 线性链表:增 删除仅处理结点,时间复杂度O(1)查找需要遍历也就是O(n) 二叉树:对一颗相对平衡的有序二叉树,对其进行插入,查找,删除,平均复杂度O(logn) 哈希表:哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)哈希表的主干就是数组

    hash冲突

    当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。

    哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式

    entry结构:

    static class Entry<K,V> implements Map.Entry<K,V> {
       final K key;
       V value;
       Entry<K,V> next;
       final int hash;
       ……
    }

    1.7 put过程 数组+链表 头插法

    public V put(K key, V value) {
       //其允许存放null的key和null的value,当其key为null时,调用putForNullKey方法,放入到table[0]的这个位置
       if (key == null)
           return putForNullKey(value);
       //通过调用hash方法对key进行哈希,得到哈希之后的数值。该方法实现可以通过看源码,其目的是为了尽可能的让键值对可以分不到不同的桶中
       int hash = hash(key);
       //根据上一步骤中求出的hash得到在数组中是索引i
       int i = indexFor(hash, table.length);
       //如果i处的Entry不为null,则通过其next指针不断遍历e元素的下一个元素。
       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;
          }
      }
      //如果遍历链表没发现这个key,则会调用以下代码
       modCount++;
       addEntry(hash, key, value, i);
       return null;
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
       Entry<K,V> e = table[bucketIndex];
       table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
       if (size++ >= threshold)
           resize(2 * table.length);
    }
    Entry( int h, K k, V v, Entry<K,V> n) {
           value = v;
           next = n;
           key = k;
           hash = h;
    }
    /*这里的next=n,说明了一切。意为: 新建节点的next,指向了n。n即为key对应索引处的链表。把之前的链表放到了新建节点的next的位置。说明是在以前链表的头部插入了新节点。故为头插法*/

    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) {
       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;
    }

    hash算法

    final int hash(Object k) {
       int h = 0;
       if (useAltHashing) {
           if (k instanceof String) {
               return sun.misc.Hashing.stringHash32((String) k);
          }
           h = hashSeed;
      }
       //得到k的hashcode值
       h ^= k.hashCode();
       //进行计算
       h ^= (h >>> 20) ^ (h >>> 12);
       return h ^ (h >>> 7) ^ (h >>> 4);
    }

    HashMap的容量

    static int indexFor(int h, int length) {  
       return h & (length-1);
    }
    为什么是2的n次方

    HashMap的resize(rehash)

    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];      
       boolean oldAltHashing = useAltHashing;        
       useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);      
       boolean rehash = oldAltHashing ^ useAltHashing;      
       transfer(newTable, rehash);  //transfer函数的调用        
       table = newTable;        
       threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);   }
    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;  //寻找到下一个节点..                
       if (rehash) {                    
       e.hash = null == e.key ? 0 : hash(e.key);               }                
       int i = indexFor(e.hash, newCapacity);  //重新获取hashcode                
       e.next = newTable[i];                  
       newTable[i] = e;              
       e = next;           }       }   }

    单线程下扩容

    多线程扩容

    假设这里有两个线程同时执行了put()操作,并进入了transfer()环节:

    刚开始:

    线程1中的e指向key(0),next指向key(4),此时线程1挂起。

    线程2调度完成所有节点的移动,移动后结果为:

    线程1继续执行,线程一会把线程二的新表当成原始的hash表,将原来e指向的key(0)节点当成是线程二中的key(0),放在自己所建table[0]的头节点。注意线程1的next仍然指向key(4),

    虽然此时key(0)的next已经是null。

    执行e.next = newTable[i],于是 key(0)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以e.next = null,
    执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(0)。好了,e 处理完毕。
    执行e = next,将 e 指向 next,所以新的 e 是 key(4)

    线程1的e指向了上一次循环的next,也就是key(4),此时key(4)的next已经是key(0)。将key(4)插入到table[0]的头节点,并且将key(4)的next设置为key(0)。此时仍然没有问题。

    现在的 e 节点是 key(4),首先执行Entry<K,V> next = e.next,那么 next 就是 key(0)了
    执行e.next = newTable[i],于是key(0) 的 next 就成了 key(4)
    执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(4)
    执行e = next,将 e 指向 next,所以新的 e 是 key(0)

    现在的 e 节点是 key(0),首先执行Entry<K,V> next = e.next,那么 next 就是 null
    执行e.next = newTable[i],于是key(0) 的 next 就成了 key(4)
    执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(0)
    执行e = next,将 e 指向 next,所以新的 e 是 key(4)

    总结版:HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

  • 相关阅读:
    GAN 的推导、证明与实现。
    WGAN学习笔记
    常用损失函数积累
    交叉熵在loss函数中使用的理解
    贝叶斯决策
    极大似然估计
    gated pixelCNN。
    三叉搜索树 转载
    Rabin-Karp 字符串匹配算法
    面试题整理 转载
  • 原文地址:https://www.cnblogs.com/luyuefei/p/12736985.html
Copyright © 2011-2022 走看看