HashMap读懂这些就够了

620 阅读5分钟

在我们这些屌丝程序员Java开发中很多人都用过HashMap但是很多人都不知道底层是如何实现的

在JDK7中采用数组+链表

在JDK8中采用数组+链表+红黑树

HashMap数据用数组存储

我们深入HashMap源码来讲解一下

进入put方法源码

public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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;
    }

这里通过indexFor方法确定下标,这里的table[i]指它自己定义的数组

int hash= key.hoshcode

int i = hash % 8

JDK7中当计算出的数组下标相同的时候,就是hash碰撞,这样我们采用链表处理

把数据存储在链表上放在链表头结点,加入头结点之后又把头结点赋值到数组的位置

置,这样就完成了新加节点的操作

resize JDK7中HashMap数组超过阀值并且都不为空才进行扩容操作

JDK7HashMap get思路:

key的hashcode计算出它应该所在的下标,再遍历这个下标的Entry链表,如果key的内存地址相等(即同一个引用)或者equals相等,则说明找到了

当key的hash没有冲突时,key在HashMap存储的位置就是匹配的node中的第一个节点。如果hash有冲突,就会在node里面节点中查询,直至匹配到相等的key

为什么HashMap的数组长度是2的次幂

为了减少Hash碰撞,尽量使Hash算法的结果均匀

当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

JDK8为什么使用红黑树

因为插入和查询的效率在链表和二叉树之间,折中选择所以选择红黑树

JDK7和JDK8HashMap的区别

1.jdk7中会将链表转变为红黑树

2.新节点插入链表的顺序不相同(jdk7是插入头节点,jdk8因为要遍历链表把链表转化为红黑树所以采用插入尾节点)

3.hash算法简化

4.resize逻辑修改(jdk7会出现死循环,jdk8不会)

JDK7是每拿到一个Node就直接插入到newTable,而JDK8是先插入到高低链表中,然后再一次性插入到newTable。 所以链表的扩容过程JDK7会出现死循环问题,而JDK8避免了这个问题。 JDK8跟原先的链表对比Node之间顺序是一致的,而JDK7是是反过来的。

为什么HashMap线程不安全

个人觉得HashMap在并发时可能出现的问题主要是两方面,首先如果多个线程同时使用put方法添加元素,而且假设正好存在两个put的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。第二就是如果多个线程同时检测到元素个数超过数组大小*loadFactor,这样就会发生多个线程同时对Node数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。

为什么HashMap的负载因子是0.75

1、负载因子是1.0

我们的数据一开始是保存在数组里面的,当发生了Hash碰撞的时候,就是在这个数据节点上,生出一个链表,当链表长度达到一定长度的时候,就会把链表转化为红黑树。

当负载因子是1.0的时候,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。

因此一句话总结就是负载因子过大,虽然空间利用率上去了,但是时间效率降低了。

2、负载因子是0.5

负载因子是0.5的时候,这也就意味着,当数组中的元素达到了一半就开始扩容,既然填充的元素少了,Hash冲突也会减少,那么底层的链表长度或者是红黑树的高度就会降低。查询效率就会增加。

但是,兄弟们,这时候空间利用率就会大大的降低,原本存储1M的数据,现在就意味着需要2M的空间。

一句话总结就是负载因子太小,虽然时间效率提升了,但是空间利用率降低了。

3、负载因子0.75

经过前面的分析,基本上为什么是0.75的答案也就出来了,这是时间和空间的权衡

怎么解决HashMap多线程情况下的现场不安全的问题

1.Map<Object, Object> map = Collections.synchronizedMap(new HashMap<>

2.Map<Object, Object> map = new ConcurrentHashMap<>();