HashMap源码解析(PUT,GET,扩容,非线程安全)

主要成员变量

  • 节点数组 table 默认容器大小capacity为16
  • 节点 ,节点成员有hash,key,value,next
  • 扩容因子loadFactor ,默认为0.75
  • 扩容节点大小threshold 计算公式为capacity*loadFactor

构造函数

HashMap提供四个构造函数

public HashMap(int initialCapacity, float loadFactor)//此构造函数传入初始容器大小和扩容因子
public HashMap(int initialCapacity)//此构造函数传入初始容器大小,扩容因子就会使用默认的0.75
public HashMap()//此构造函数是无参的,只会设置扩容因子为默认的0.75,而容器大小是不会初始化的,等PUT的时候再扩容,下文介绍。
public HashMap(Map<? extends K, ? extends V> m)//此构造函数是传入一个初始map,容器大小会初始化为大于此map大小的最小的2的幂次方的数,如传入map的大小为10,则容器大小为16.

PUT方法

1、判断容器是否为空,如果为空,则进行扩容(扩容方法详解下面介绍)。
2、根据key获取的hash值(下面介绍)跟容器大小-1(n-1)取模获取到容器下标,根据下标在节点容器中查找。
a.如果没有找到,则直接新建一个节点放入容器
b.如果有找到,且这个节点是红黑树节点,则以红黑树的方式进行插入新节点。且这个节点的hash,key都一致,则将旧值替换为新值,且返回旧值。如果在这个节点链表中没找到这个key,且已有的节点链表长度小于7,则直接在这个节点末尾直接插入这个新节点(尾插法),如果已有的节点长度等于7(加上新加的这个节点等于8),则会将这个节点链表变为红黑树。以红黑树的方式插入。
3、如果加入后的节点数量大于扩容节点数量(默认为12)则需要扩容。
流程图如下:
在这里插入图片描述

key的hash算法
//扰动函数
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
   tab[i = (n - 1) & hash]

这是用了二次hash运算。
第一次是通过key的hashcode无符号右移16位与key的hashcode做异或运算,这样能保证高16位能参与第二次hash(&)运算(如果直接用hashcode与n-1进行&运算,因为n-1的前16位基本都是0,则hashcode的高位基本不影响最终的结果),混合原始哈希码的高位和低位,以此来加大低位的随机性,使最终二次hash计算出来的hash值碰撞减少。
第二次是为了能使计算的hash值能落在容器table中,所以需要对table的长度取模 hash%n;
计算机中,&运算比%运算高很多,所以基于以下的公式:
当 lenth = 2^n 时,X % length = X & (length - 1)
为了满足这个公式,让&取代%,我们容器的大小必须是2的幂次方,而且每次扩容都是扩大一倍。这也说明了为啥上面构造函数,如果你传的容器大小是18的话,他也会把你设为32。

扩容(resize)

在上面,我们知道,在put的时候有多次地方用到扩容。扩容源码如下(已添加了注释)

 final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //不是第一次扩容
        if (oldCap > 0) {
        //如果已有的tabledable大小已经是最大的,无法扩容,直接返回
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //newCap = oldCap << 1这里把容器扩大两倍,在上面有说明为啥扩大两倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 相应的扩容节点大小也扩大两倍
        }
        else if (oldThr > 0)  //是第一次扩容,且是通过自己传入容器大小来初始化的,则直接把初始容器大小设为自己传入的容器大小
                    newCap = oldThr;
        else {               // 是第一次扩容,且是无参构造函数初始化的,用默认的大小
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {//是第一次扩容,且是通过自己传入容器大小来初始化的,因为此时oldThr是等于容器大小,不能直接赋值,则需要再根据公式容器大小*加载因子 计算一遍
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {//不是第一次扩容,则需要将原来的节点重新进行hash计算
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)//节点链表就一个节点,则直接计算hash存储
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//红黑树节点,则需要将红黑树分组,重新计算
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // 分组链表
                        Node<K,V> loHead = null, loTail = null;//重新计算后的hash下标值不变
                        Node<K,V> hiHead = null, hiTail = null;//重新计算后的hash下标值等于原来的hash下标+oldCap
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//下标不变,使用尾插法,避免了多线程死循环
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

每次扩容都是以两倍的大小扩容,且如果不是第一次扩容,则需要重新计算hash值排列,这是很耗费资源的,所以如果初始大概知道需要用到的容器大小,我们可以先初始设置容器大小,这样就能减少扩容的次数了。

多线程下put引发的线程不安全问题

在put方法中有一个判断是否hash碰撞

if ((p = tab[i = (n - 1) & hash]) == null)// A
            tab[i] = newNode(hash, key, value, null);

假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完上面第A行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所以此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

if (++size > threshold)//B
            resize();

除此之前,还有就是代码中最后有一个判断是否需要扩容的第B行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第B行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所以说还是由于数据覆盖又导致了线程不安全。

GET方法

源码如下,已添加注释,比较简单,只是涉及到链表查找以及红黑树查找。

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
 final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //hash碰撞有值
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//hash跟key都一样,则返回
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)//红黑树节点,则需要再红黑树中查找节点
                    return ((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、hashMap并不是在链表元素个数大于8就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换。
2、hashMap的红黑树不一定小于6的时候才会转换为链表,而是只有在resize的时候才会根据 UNTREEIFY_THRESHOLD(6) 进行转换。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值