美文网首页
HashMap和ConcurrentHashMap

HashMap和ConcurrentHashMap

作者: 改名_f64e | 来源:发表于2019-09-26 20:26 被阅读0次

    Hashmap

    26341ef9fe5caf66ba0b7c40bba264a5_hd.png
    
    1.HashMap:
      它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,
      因而具有很快的访问速度,但遍历顺序却是不确定的.
      HashMap最多只允许一条记录的键(Key)为null,允许多条记录的值(Value)为null.
      HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致.
      如果需要满足线程安全,可以用 Collections.synchronizedMap(map)或者ConcurrentHashMap.
    
    2.Hashtable:
      Hashtable继承自Dictionary类,是线程安全的,任一时间只有一个线程能写Hashtable,
      并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁.
      Hashtable不建议在新代码中使用,需要线程安全用ConcurrentHashMap,否则用Hashmap
    
    3.LinkedHashMap:
      LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,
      先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
    
    4.TreeMap:
      TreeMap实现SortedMap接口,能够把它保存的记录根据键(Key)排序,默认是按键值的升序排序,
      也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的.
      如果使用排序的映射,建议使用TreeMap.
      在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,
      否则会在运行时抛出java.lang.ClassCastException类型的异常.
    
    HashMap - 存储结构
      Hashmap在JDK1.7中的结构是数组+链表
      Hashmap在JDK1.8中的结构是数组+链表+红黑树结构(当链表长度大于8时转换成红黑树)
     
    HashMap - 重要字段
      int threshold;// 所能容纳的key-value对极限
        threshold = length(table的长度) * loadFactor
      final float loadFactor;// 负载因子,默认0.75
        loadFactor = 1//表示当所有的table位置都被使用后才进行扩容,等于 时间换空间
        loadFactor = 0.5//表示当table用了一半的位置,就进行扩容,等于 空间换时间
      int modCount;//HashMap结构变化的次数
        put一个已经存在的值,不属于结构改变
        put一个不存在的key-value,属于结构改变
      int size;//当前HashMap的实际的长度,不是table的长度
      
      transient Node<K,V>[] table;//Node桶数组,数据存放在这里
      默认 length(table的长度) = 16
      static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;//Key的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;
            }
      }
    
    HashMap - Key位置确定
        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
      通过hashcode()方法获取对应Key的hashcode的值
        1111 1111 1111 1111 1111 0000 1110 1010 
        高16 = 1111 1111 1111 1111 
        低16 = 1111 0000 1110 1010 
        
        高16位不变 ,低16位异或高16位
        1111 1111 1111 1111 1111 0000 1110 1010
        0000 0000 0000 0000 1111 1111 1111 1111
        =
        1111 1111 1111 1111 0000 1111 0001 0101
        
        再与n-1进行与计算(n = hashmap的长度,-1是因为角标小1)
        0000 0000 0000 0000 0000 0000 0000 1111
        1111 1111 1111 1111 0000 1111 0001 0101
        =
        0000 0000 0000 0000 0000 0000 0000 0101 = 5(Key所在的位置)
    
    HashMap - put流程(具体看源码)
      1.调用putVal()
      2.判断table == null || length== 0
        是:调用resize() 扩容
      3.通过Key的HashCode值,计算Key所在的数组位置
      4.判断table[i] == null
        是:调用newNode(),直接插入数据
      5.判断table[i] 链表上第一个元素是否等于当前的元素
        是:直接覆盖value
      6.判断table[i] 是不是treeNode类型
        是:红黑树直接插入数据
      7.开始遍历链表插入数据,链表长度是否大于8
        是:链表转换成红黑树,插入数据
      8.链表插入,若Key存在,直接覆盖value
        否则,默认放在第一位,next指向原来的第一位
      
      无效扩容问题:
        在putVal()中,最后会判断是否需要扩容:
        if (++size > threshold)
                resize();
        极端例子:这里判断扩容是根据size,而不是table.length,如果所有的key都放在数组的同一个位置
          会导致链表极大,size也会变大,此时数组其他空间虽然没有数据,但是还是会不断扩容
        
    HashMap - 链表存储顺序
      put -> 5,7,3(假设在数组同一个位置上)
      链表结构 -> 3(next -> 7(next -> 5)), 3,7,5的反序
    
    HashMap - 扩容机制(JDK1.7,JDK1.8加入了红黑树,略有不同) 
      void resize(int newCapacity) {   //传入新的容量
         Entry[] oldTable = table;    //引用扩容前的Entry数组
         int oldCapacity = oldTable.length;   
         //扩容前的数组大小如果已经达到最大(2^30)了      
         if (oldCapacity == MAXIMUM_CAPACITY) { 
              //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
             threshold = Integer.MAX_VALUE; 
             return;
         }
          //初始化一个新的Entry数组
         Entry[] newTable = new Entry[newCapacity];  
          //!!将数据转移到新的Entry数组里
         transfer(newTable);                         
           //HashMap的table属性引用新的Entry数组
         table = newTable;                          
         threshold = (int)(newCapacity * loadFactor);//修改阈值
      }
      void transfer(Entry[] newTable) {
         Entry[] src = table;                   //src引用了旧的Entry数组
         int newCapacity = newTable.length;
         for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
             Entry<K,V> e = src[j];             //取得旧Entry数组的每个元素
             if (e != null) {
                  //释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
                 src[j] = null;
                 do {
                     Entry<K,V> next = e.next;
                      //!!重新计算每个元素在数组中的位置
                     int i = indexFor(e.hash, newCapacity); 
                     e.next = newTable[i]; //标记[1]
                     newTable[i] = e;      //将元素放在数组上
                     e = next;             //访问下一个Entry链上的元素
                 } while (e != null);
             }
         }
       }
    
    HashMap - 为什么table默认长度是16,每次是原来的两倍
      HashMap 每次扩容是2(n)
      在初始化中new HashMap(20),调用tableSizeFor()
      并在第一次put()时候进行resize():
        Node[].length(table.length) = 32, threshold = (int)(32*0.75)=24
      0 -> 1,1 -> 1,
      2 -> 2, 
      3 -> 4,4 -> 4
      5 -> 8,6 -> 8,7 -> 8,8 -> 8
      9 -> 16,10 -> 16,11 -> 16,12 -> 16,13 -> 16,14 -> 16,15 -> 16,16 -> 16
      17 -> 32
      
      table.length 总是 = 2(n),假设table.length = 16
      n = 16 - 1= 15
        0000 0000 0000 0000 0000 0000 0000 1111
      n = 32 -1 = 31
        0000 0000 0000 0000 0000 0000 0001 1111
      相当于每次扩容后length的二进制的值只是改变了1位
      重新计算Key所在的位置时更快:
      直接取出e.hash的值:
        0000 0000 0000 0000 0000 0000 0000 0101 = 5
        0000 0000 0000 0000 0000 0000 0001 1111
        
        0000 0000 0000 0000 0000 0000 0001 0101 = 21 = 5+16
        1.位置相当于在原来的基础上增加了扩容的值(而不会是随机值),计算快
        2.更好的随机分配数据所在的数组位置,劲量避免hash冲突
    
    HashMap - 线程不安全
      在多线程的情况下存数据,如果刚好发生了resize(),此时需要重新计算位置
      有可能会造成链表死循环,即next互相指向对方
    
    

    ConcurrentHashMap

    存储 :
      key != null,value != null
    
    put : 逻辑
      final V putVal(K key, V value, boolean onlyIfAbsent) {
            //判断key和value是否为null,抛出异常
            if (key == null || value == null) throw new NullPointerException();
            //计算hash值
            int hash = spread(key.hashCode());
            int binCount = 0;
            //对当前的table进行循环
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //初始化table
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                //通过hash值原子查询当前位置上数据是否==null,-1的二进制是32个1
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    //通过cas对数据进行赋值
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                //多线程扩容时,帮助扩容
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;
                    //当前数组上的hash值相同的Node,第一个元素作为锁,锁住整个链表
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                        //...操作
                            }
                    }
                }
            }
            addCount(1L, binCount);
            return null;
        }
    

    相关文章

      网友评论

          本文标题:HashMap和ConcurrentHashMap

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