java8的hashmap

HASHMAP

JAVA8对HashMap的调整在两个方面,一是当链表中的元素超过了 8 个以后,会将链表转换为红黑树

二是新的键值对会插入到链表尾部而不是头部。

HASHMAP

    Treemap和LinkedHashMap如何保证顺序的?

        Treemap通过实现SortMap接口,能够把它保存的键值对根据key排序,基于红黑树,从而保证Treemap中所有键值对处于有序状态;

        LinkedHashMap则是通过插入排序(put的时候顺序是什么,取出来的就是什么顺序)和访问排序(改变顺序把访问过的放到底部)让键值有序

Hashtable为什么不能接受null建和值?

        因为equals()方法需要对象,不能为空

减少碰撞的方法?

    扰动函数;原理:如果两个不相等的对象返回不同的hashcode,那么碰撞的几率就会小,这意味着存链表结构减小,取值时就不用频繁调用equals方法,提高HashMap的性能(扰动即Hash方法内部的算法实现,目的是让不同对象返回不同的hashcode)

    使用不可变,声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生。不可变性能够缓存不同建的hashcode,将提高整个获取对象的速度。

为什么选择红黑树而不用二叉查找树?

    二叉查找树在特殊情况下回变成线性结构;引入红黑树是为了查找数据快,解决链表查询深度的问题;

红黑树?

    1.每个节点非黑即红;2.根节点总是黑色的;3.如果节点是红色的,则它的子节点是黑色的(反之不一一定);4.每个叶子节点都是黑色的空节点(NIL节点);5.从根节点到业绩诶单或空子节点的每条路径,必须包含相同数据的黑色节点(既相同的黑色高度)

    hshtable默认容量:11

    ConcurrentHashMap原理: 引入了CAS(内存值和预期值相同时,才会更改为新值)对sizeCtl的控制都是由CAS来实现的。默认为0;

        对变量增加一个版本号,每次修改,版本号+1,比较的时候比较版本号;

    ConcurrentHashMap同步性能更好,因为仅仅根据同步级别对map的一部分进行上锁;

当HashTable的大小增加到一定程度时,性能下降,因为迭代时需要被锁定很长时间。(锁定整个map)

   而ConcurrentHashMap引入了分割,无论多大,仅仅需要锁定map的某个部分,而其他的线程不需要等到迭代完整才能访问map。

hashmap

  • 使用哈希表(散列表)来进行数据存储,并使用链地址法来解决冲突
  • 当链表长度大于等于 8 时,将链表转换为红黑树来存储
  • 每次进行二次幂的扩容,即扩容为原容量的两倍

treemap

  • TreeMap 中保存的记录会根据 Key 排序(默认为升序排序),因此使用 Iterator 遍历时得到的记录是排过序的
  • 因为需要排序,所以TreeMap 中的 key 必须实现 Comparable 接口,否则会报 ClassCastException 异常
  • TreeMap 会按照其 key 的 compareTo 方法来判断 key 是否重复

 hash(Object key)

HashMap 便是通过 hashCode 来确定一个 key 在数组中的存储位置。

HashMap 并非直接使用 hashCode 作为哈希值,而是通过这里的 hash 方法对 hashCode 进行一系列的移位和异或处理,这样处理的目的是为了有效地避免哈希碰撞

static final int hash(Object key) {

    int h;

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

put(K key, V value)

public V put(K key, V value) {

    return putVal(hash(key), key, value, false, true);

}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    HashMap.Node<K, V>[] tab;
    HashMap.Node<K, V> p;
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        //初始化哈希表
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        //通过哈希值找到对应的位置,如果该位置还没有元素存在,直接插入
        tab[i] = newNode(hash, key, value, null);
    else {
        HashMap.Node<K, V> e;
        K k;
        //如果该位置的元素的 key 与之相等,则直接到后面重新赋值
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof HashMap.TreeNode)
            //如果当前节点为树节点,则将元素插入红黑树中
            e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        else {
            //否则一步步遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //插入元素到链尾
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        //元素个数大于等于 8,改造为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                //如果该位置的元素的 key 与之相等,则重新赋值
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //前面当哈希表中存在当前key时对e进行了赋值,这里统一对该key重新赋值更新
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //检查是否超出 threshold 限制,是则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
 

在 Java 8 之前,HashMap 插入数据时一直是插入到链表表头;而到了 Java 8 之后,则改为了尾部插入。至于头插入有什么缺点,其中一个就是在并发的情况下因为插入而进行扩容时可能会出现链表环而发生死循环;当然,HashMap 设计出来本身就不是用于并发的情况的。

链表中元素大于等于 8,这时有可能将链表改造为红黑树的数据结构??

MIN_TREEIFY_CAPACITY 规定了 HashMap 可以树化的最小表容量为 64,这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化

HashMap 为什么要进行树化呢?我们都知道,链表的查询效率大大低于数组,而当过多的元素连成链表,会大大降低查询存取的性能;同时,这也涉及到了一个安全问题,一些代码可以利用能够造成哈希冲突的数据对系统进行攻击,这会导致服务端 CPU 被大量占用。

resize()

resize方法的三个if-else if-else

1. 表项只有一个键值对时,针对新表计算新的索引位置并插入键值对

2. 表项节点是红黑树节点时(说明这个bin元素较多已经转成红黑树了),调用split方法处理。

3. 表项节点包含多个键值对组成的链表时(拉链法)

第一种情况就是直接对新的数组长度取模计算新索引,放到新数组的相应位置,和jdk7一样的。第二种情况是引入了红黑树后独有的,通过调用一个split方法处理,关于这个方法一会再细说。第三种情况在jdk7里没引入树时也有,但是jdk8里对这种情况也做了算法上的优化。

当要存储的元素超过阈值时,则进行2倍扩容,将原来map中的数据再次重新求hash索引值插入新的数组中

java8:

2、对key值得hash值和旧数组大小进行&与运算,如果结果为0,索引位置不变,还是旧索引位置,不为0则表示需要移位,新位置为原先位置+旧数组的小大(新数组大小为旧数组翻倍),效率比Java7高。额外提一点,Java的链表节点数超过8个时,会将链表转化为红黑树,

get(Object key)

final HashMap.Node<K,V> getNode(int hash, Object key) {
    HashMap.Node<K,V>[] tab; HashMap.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 && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //否则检查是否为树节点,则调用 getTreeNode 方法获取树节点
            if (first instanceof HashMap.TreeNode)
                return ((HashMap.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;
}
 

主要就四步:

  1. 哈希表是否为空或者目标位置是否存在元素
  2. 是否为第一个元素
  3. 如果是树节点,寻找目标树节点
  4. 如果是链表结点,遍历链表寻找目标结点

JDK7中HashMap采用的是位桶+链表的方式。而JDK8中采用的是位桶+链表/红黑树的方式,当某个位桶的链表的长度超过8的时候,这个链表就将转换成红黑树。因为引入了树, 到了8以后,就要判断是链表还是树,如果是链表,插入后还要判断要不要转化成树。不过这些操作都是常量级别的,复杂度还是O(1)的,但是对整体性能提升非常大。

Java7中对于<key1,value1>的put方法实现相对比较简单,首先根据 key1 的key值计算hash值,再根据该hash值与table的length确定该key所在的index,如果当前位置的Entry不为null,则在该Entry链中遍历,如果找到hash值和key值都相同,则将值value覆盖,返回oldValue;如果当前位置的Entry为null,则直接addEntry。

java8不是用红黑树来管理hashmap,而是在hash值相同的情况下(且重复数量大于8),用红黑树来管理数据。 红黑树相当于排序数据。可以自动的使用二分法进行定位。性能较高。这一点在hash不均匀并且元素个数很多的情况时,对hashmap的性能提升非常大

一般情况下,hash值做的比较好的话基本上用不到红黑树。

对线程安全性要求高的时候可以用同步包装器(Collections.synchronizedMap())包装一个线程安全的hashmap。通过这种方式实现线程安全,所有访问的线程都必须竞争同一把锁,不管是get还是put。好处是比较可靠,但代价就是性能会差一点。

ConcurrentHashmap通过分段锁技术提高了并发的性能,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。另外concurrenthashmap的get操作没有锁,是通过volatile关键字保证数据的内存可见性。所以性能提高很多。JDK8对ConcurrentHashmap也有了巨大的的升级,同样底层引入了红黑树,并且摒弃segment方式,采用新的CAS算法思路去实现线程安全,再次把ConcurrentHashmap的性能提升了一个台阶。

jdk7里hashmap resize时对每个位桶的链表的处理方式(transfer方法),整体过程就是先新建两倍的新数组,然后遍历旧数组的每一个entry,直接重新计算新的索引位置然后头插法往拉链里填坑(这里因为是新加入的元素插入到链表头,所以顺序会倒置,jdk8里不会)

jdk8的代码里是这么处理的,把链表上的键值对按hash值分成lo和hi两串,lo串的新索引位置与原先相同[原先位置j],hi串的新索引位置为[原先位置j+oldCap]。这么做的原因是,我们使用的是2次幂的扩展(newCap是oldCap的两倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置,也就是原索引+oldCap。为啥?自己举个例子就知道了。那怎么判断该假如lo串还是hi串?这个取决于 判断条件if ((e.hash & oldCap) == 0),如果条件为真,加入lo串,条件为假,加入hi串。那这是为什么?因为这个&运算其实相当于做了一个掩码,查看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。

 https://www.jianshu.com/p/4177dc15d658  简书的java8对hashmap的改进

https://my.oschina.net/xianggao/blog/393990

Java的文档说HashMap是非线程安全的,应该用ConcurrentHashMap。

hashmap用于单线程, 程序可能Hang在了HashMap.get()这个方法上, 占了100%的CPU(增加 synchronized 。恢复正常)

 https://blog.csdn.net/u011535541/article/details/80217128  java 7,8 对hashmap的对比

hashmap的 扩容放值   http://www.iteye.com/topic/539465

  1. HashMap 在 new 后并不会立即分配bucket数组,而是第一次 put 时初始化,类似 ArrayList 在第一次 add 时分配空间。
  2. HashMap 的 bucket 数组大小一定是2的幂,如果 new 的时候指定了容量且不是2的幂,实际容量会是最接近(大于)指定容量的2的幂,比如 new HashMap<>(19),比19大且最接近的2的幂是32,实际容量就是32。
  3. HashMap 在 put 的元素数量大于 Capacity LoadFactor(默认16 0.75) 之后会进行扩容。
  4. JDK8在哈希碰撞的链表长度达到TREEIFY_THRESHOLD(默认8)后,会把该链表转变成树结构,提高了性能。
  5. JDK8在 resize 的时候,通过巧妙的设计,减少了 rehash 的性能消耗。
  1. javahashmap条目   https://yq.aliyun.com/articles/225660?utm_content=m_32836

put函数的思路大致分以下几步:

  1. 对key的hashCode()进行hash后计算数组下标index;
  2. 如果当前数组table为null,进行resize()初始化;
  3. 如果没碰撞直接放到对应下标的bucket里;
  4. 如果碰撞了,且节点已经存在,就替换掉 value;
  5. 如果碰撞后发现为树结构,挂载到树上。
  6. 如果碰撞后为链表,添加到链表尾,并判断链表如果过长(大于等于TREEIFY_THRESHOLD,默认8),就把链表转换成树结构;
  7. 数据 put 后,如果数据量超过threshold,就要resize。

resize()用来第一次初始化,或者 put 之后数据超过了threshold后扩容

数组下标计算: index = (table.length - 1) & hash  ,由于 table.length 也就是capacity 肯定是2的N次方,使用 & 位运算意味着只是多了最高位,这样就不用重新计算 index,元素要么在原位置,要么在原位置+ oldCapacity。

如果增加的高位为0,resize 后 index 不变,

这个设计的巧妙之处在于,节省了一部分重新计算hash的时间,同时新增的一位为0或1的概率可以认为是均等的,所以在resize 的过程中就将原来碰撞的节点又均匀分布到了两个bucket里。

hash

JKD7 中,bucket数组下标也是按位与计算,但是 hash 函数与 JDK8稍有不同,

  • hash为了防止只有 hashCode() 的低 bit 位参与散列容易碰撞,也采用了位移异或,只不过不是高低16bit,而是如下代码中多次位移异或。
  • JKD7的 hash 中存在一个开关:hashSeed。开关打开(hashSeed不为0)的时候,对 String 类型的key 采用sun.misc.Hashing.stringHash32的 hash 算法;对非 String 类型的 key,多一次和hashSeed的异或,也可以一定程度上减少碰撞的概率。
  • JDK 7u40以后,hashSeed 被移除,在 JDK8中也没有再采用,因为stringHash32()的算法基于MurMur哈希,其中hashSeed的产生使用了Romdum.nextInt()实现。Rondom.nextInt()使用AtomicLong,它的操作是CAS的(Compare And Swap)。这个CAS操作当有多个CPU核心时,会存在许多性能问题。因此,这个替代函数在多核处理器中表现出了糟糕的性能。

hashSeed 默认值是0,也就是默认关闭,任何数字与0异或不变。hashSeed 会在capacity发生变化的时候,通过initHashSeedAsNeeded()函数进行计算。当capacity大于设置值Holder.ALTERNATIVE_HASHING_THRESHOLD后,会通过sun.misc.Hashing.randomHashSeed产生hashSeed 值,这个设定值是通过 JVM的jdk.map.althashing.threshold参数来设置的,

猜你喜欢

转载自blog.csdn.net/qq_21325705/article/details/84236531