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是线程不安全的。

  • 相关阅读:
    webpack基础
    LeetCode232. 用栈实现队列做题笔记
    mysql 时间加减一个月
    leetcode 1381. 设计一个支持增量操作的栈 思路与算法
    LeetCode 141. 环形链表 做题笔记
    leetcode 707. 设计链表 做题笔记
    leetcode 876. 链表的中间结点 做题笔记
    leetcode 143. 重排链表 做题笔记
    leetcode 1365. 有多少小于当前数字的数字 做题笔记
    LeetCode1360. 日期之间隔几天 做题笔记
  • 原文地址:https://www.cnblogs.com/luyuefei/p/12736985.html
Copyright © 2011-2022 走看看