HashMap底层源码解析

所有人都知道,HashMap底层的数据结构是数组+链表+红黑树

HashMap<String, Objeck> map = new HashMap<>();
map.put(“1”,“zhangsan”);
当我们在执行put的过程中,它的底层是怎样运作的呢?
在这里插入图片描述

基本执行流程:

HashMap在初始化的时候,默认是一个数组,他在put 的过程中会进行计算hash值,按照hash值进行存储,
如果计算之后,hash值相同,他会判断内容是否相同,如果相同则覆盖,如果不同则在数组下面以链表的形式进行添加,当链表的长度达到一定的阀值的时候,链表会自动转换为红黑树。

上图中,每一个小格子都代表了hashmap中一个内部类对象,表示一个节点 包含【key,value】,存储了键和值
class Node{
private String key;
private String value;
}

数组的表示形式:
Node []node = new node[16] ------->表示了数组
链表的表示形式:
class Node{
private String key;
private String value;
private Node next; //表示了一个单向的链表
}

红黑树的表示形式:
class TreeNode extends Node{
Node parent;
Node left ; //这三个属性表示树的三个节点
Node right;
}

hash值的计算过程

HashMap在存储过程中,最重要的无非就是获取到要存储的数组位置下标
要想确定存储的位置,首先需要确定2点
1.数组的长度length
2.下标范围(0到length-1)

下标范围在计算的时候,他首先会根据int hash = hashCode()函数得到一个32位的值

如果在我们的想法里面,用 hash值%length不就是这个范围吗?
所以在Node类中,还应该有一个int hash; 用来存储hash值
因为在hash值确定了,对应的下标也就确定了。

但实际上上述的算法并不合适,因为这样造成的重复率太高了,假设length是16,hash值为16,32,64…所计算出的下标就会完全一致,所以他重复的概率太高了,在java中则是运用下面的方式来计算的。

在HashMap的算法中,使用的&(与运算)来代替%运算,这种方式造成的重复率更低,也就更加安全

举例:
hash1: 0000 1111 0000 1111 1010
hash2:0001 1111 0001 1101 1101
假设hashmap容量为16 length -1 值为(16-1):0000 1111

hash1 & (length - 1) = 0000 0000 0000 0000 1010 = 10
hash2 &(length - 1) = 0000 0000 0000 0000 1101 = 13
这样可以保证hash值得到的结果肯定是数组的下标值,并且尽量减少碰撞

这时候又会抛出一个问题,为什么HashMap的容量总是 “2的几次方” 呢?

做一个假设:
假设HashMap的长度为15 ( length-1) = 1110
hash1 = 0000 1111
hash2 = 1111 1110

hash1 % ( length-1) = 1110
hash2 % ( length-1) = 1110
这样的话,造成最后一位不管什么情况下都等于0,增大了碰撞的概率,而长度如果正好是2的n次方,
则可以解决这个问题
例:长度为16: (length -1)= 1111
例:长度为32:(length -1 ) = 11111
所以这个时候就可以保证,下标位置最终计算结果依赖的是hash值,与计算公式的(length -1)的结果无关
在这里插入图片描述
但是上述的结果还是有很大的重复率,例如:
hash1 = 0000 1010
hash2 = 1100 1010

虽然hash值并不相同,但是结果还是会重复,所以这时候java又在计算hash值之前增加了一个“ ^ ”运算
先将hash值>>>16(低16位运算) 然后用原先的hash ^ 位移的hash,最终算出hash值,然后在进行上面的&(与)运算,这样,最终就最大程度的减少了下标重复的概率
在这里插入图片描述
所以indexx下标的完整算法是:key.hashCode() 然后高16位和低16为进行异或运算 ,然后再和(length -1)进行(&)运算 这样最终得到index结果值,这样得到的重复的可能性大大降低。

put的过程

在这里插入图片描述
在调用put的时候,首先会调用hash()方法得到hash值

final V putVal(int hash, K key, V value, boolean  onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //  判断map是否是null
        if ((tab = table) == null || (n =  tab.length) == 0)
            n = (tab = resize()).length;

        // 计算index位置,并判断这个位置是否有节点,如果没有直接赋值
        if ((p = tab[i = (n - 1) & hash]) ==  null)
            tab[i] = newNode(hash, key, value,  null);
        else {
            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 TreeNode)
                e =  ((TreeNode<K,V>)p).putTreeVal(this, tab, hash,  key, value);
            else {
                //key值不同,按照链表方式存储----->循环遍历找到最后一个节点,并存储
                for (int binCount = 0; ;  ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key,  value, null);
                        // 但凡节点超过一个长度,就给他转换为红黑树
                        if (binCount >=  TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key  != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping  for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue ==  null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

根据hash结果,计算node的位置,
判断index这个位置有没有节点,如果没有,就直接放入这个位置
index这个位置如果有元素,判断key
key相同,直接替换value值
key不同,按照链表存储
key不同,按照红黑树存储

HashMap扩容

数组长度不够用?
本质上是创建一个新的数组,然后将老的数组中【链表,红黑树】,迁移到新的数组中去
什么情况会进行扩容呢?
整个数据结构中节点超过length*0.75就会进行扩容
源码:
if (++size > threshold)
resize();
他就会执行扩容操作

resize()方法内:

if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

他扩容的方式是进行位移
原来是1<<4 10000 <<1 100000 2^5 充分利用了位移的高效率
这里,也就保证了HashMap的长度永远是2的n次方

创建成功后,就会对数据进行迁移
1.循环遍历旧数组下标
2.当下标位置有元素,进行迁移,
3.如果有元素,并且下面没有元素

@SuppressWarnings({"rawtypes","unchecked"}
          // 首先创建新的数组
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            // 循环遍历所有旧节点
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //判断当前下标是否有元素,没有元素不值得迁移
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 该节点下面如果没有后续节点
                    if (e.next == null)
                     // 再次用新的算法得到index下标值,并进行赋值
                        newTab[e.hash & (newCap - 1)] = e;
                     //如果后续节点是红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //如果下面是链表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //循环链表
                        do {
                            next = e.next;
                           //老数组链表中的node元素,会保存到新数组对应位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                             //老数组的node元素,保存到index+老长度的位置
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }

线程安全问题

jvm在运行时,会有一个主内存
每个线程在运行时,也会有每个线程的工作内存,线程在访问主线程的HashMap时候,会先将数据拷贝到工作内存,然后再将数据写回去,这个时候如果有其他线程在这个过程中读取数据,就会造成数据不一致的情况

多线程执行操作和单线程执行操作,最终的数据不一致,线程非安全

一般情况下,我们可以通过为它加锁保证线程的安全性
put方法---------> synchronized :一个线程操作完成,其他线程才能进入操作

HashMap的线程安全解决方案,Hashtable
put方法加了synchronized

但是这样由于方法加了锁,所以对于效率来说比较低下,因为一旦有一个线程在操作,那么整个map其他线程都不能操作,只能等待,这个时候效率太低下了

java中也给出了对应的优化方案:

我们可以只给他在你指定需要操作的数组下标加锁,意思是,某个hash值对应的下标加锁了,表示你正在操作这个下标,但对于其他的下标别人也可以正常操作,并不受影响,只是别人不能操作你这个下标下面的链表和红黑树了,

这就是HashMap的线程安全问题优化的一个解决方案,ConcurrentHashMap类,只在当前下标区域控制线程安全
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值