HashMap-01

Jingxc大约 21 分钟java后端HashMapjava后端

HashMap-01

HashMap基础

HashMap存储结构

在JDK1.7中和JDK1.8中有所区别: 在JDK1.7中,由”数组+链表“组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。

在JDK1.8中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响HashMap的性能,红黑树搜索时间复杂度是O(logn),而链表是O(n)。因此,JDK1.8对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:

当链表超过8且数组长度(数据总量)超过64才会转为红黑树 将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。 ArrayList底层是动态数组数据结构,查询快,增删慢(因为Array增删需要移动数据),LinkedList底层是双重链表数据结构,查询相对较慢(因为Link查询需要移动指针),增删块。

HashMap的node属性


HashMap有一个重要的属性Node<K,V>,Node是HashMap的一个内部类实现了Entry接口,本质是一个映射

Node类基本属性有:

hash:key的哈希值
key:节点的key,类型和定义HashMap时的key的类型一致
value:节点的value,类型和定义HashMap时的value的类型一致
next:该节点的下一个节点

next属性记录的是下一个节点本身,也是一个Node节点,这个Node节点也有next属性,也记录了下一个节点,这样的一个链表结构,对于一个HashMap来说,只需要明确记录每个链表的第一个节点,就能顺序遍历链表的所有节点。

HashMap的容量


HashMap的默认容量16

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY =1<<4;//aka 16

HashMap的负载因子


HahhMap的默认加载因子是0.75

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR =0.75

当HashMap的元素超过容量*加载因子时,HashMap会进行扩容

HashMap的hash算法


HashMap的hash返回值:

问题:HashMap⾥⾯的hash()返回值为什么不是的返key.hsahCode()的返回值,⽽是key.hsahCode()^(key.hsahCode()>>>16)的返回值呢?

static final int hash(Object key){
    int h;
    return(key ==null)?0:(h = key.hashCode())^(h >>>16);
}

提示

这样做的目的是减少hash的冲突概率

HashMap在put的时候,hash冲突时不可避免的,所以如何尽量避免hash冲突,或者在hash冲突时如何快速高效定位到数据的真实存储位置,就是HashMap中最核心的部分key.hsahCode()^(key.hsahCode()>>>16)的逻辑就是先获得key的hashCode值h,然后h和h右移16位做异或运算,这样高16为也参与到hash的逻辑运算中了,减少冲突。

比如有两个key的hashCode返回值如下:

key1.hashCode()

1111 1111 1111 1111 0101 0101 0111 0101

key2.hashCode()

1111 1111 1110 1111 0101 0101 0111 0101

如果没有^(key.hsahCode()>>>16)那么,15&hash就分别是

key1在底层的数组索引值是:5

1111 1111 1111 1111 0101 0101 0111 0101

0000 0000 0000 0000 0000 0000 0000 1111

key2在底层的数组索引值是:5

1111 1111 1110 1111 0101 0101 0111 0101

0000 0000 0000 0000 0000 0000 0000 1111

这是因为key1.hashCode()和key2.hashCode()的低16位完全相同,然后15&hash的时候,15的二进制00000000000000000000000000001111的高位全是0,这就造成了15&hash的时候,只有hash的低16为起了作用,而key1.hashCode()和key2.hashCode()的低16位完全相同,所以底层索引值也相同了,这样很容易造成hash冲突。但如果有^(h >>>16)

相关信息

比如有两个key的hashCode返回值如下:

就比如key.hsahCode()也就是h的变量值如下:

1111 1111 1110 1111 0101 0101 0111 0101

然后(h = key.hashCode())^(h >>>16)就变成

1111 1111 1110 1111 0101 0101 0111 0101^

0000 0000 0000 0000 1111 1111 1110 1111

计算结果是:1111 1111 1110 1111 1010 1010 1001 1010

这里可以看出,最终的结果值具备了高16位与低16位共同的特征,这样减少了hash冲突的概率。

HashMap的数组+链表/树


HashMap为什么要引入链表?

HashMap底层是数组,当map进行put操作的时候,会进行hash计算,判断这个对象属于数组的哪个位置,当多个对象在数组的同一位置时,就会有hash冲突,这个时候就引入了链表。

为什么jdk1.8会引入红黑树?

当链表长度大于8时,遍历查找效率较慢,故引入红黑树。并不是只需要链表长度大于8 ,同时需要满足数组长度大于64;还有如果红黑树的节点个数小于6时,红黑树会变成链表。

HashMap为什么不一开始就使用红黑树


这是因为红黑树维护成本比较大,红黑树在插入新数据后,可能会通过左旋,右旋,变色来保持平衡,造成维护成本过高,故链路较短时采用链表。

HashMap底层数组取值时为什么不用取模而是&


tab[i = (n-1) & hash],这是因为在计算机运算时,使用&比取模性能更快,

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

总共有3个好处:

  • 为了减少hash冲突,就是为了数据均匀分布,此时我们一般使用公式(hashCode%size),这样可以达到最大平均分配,而(n-1)& hash,当n为2的次幂时,会满足一个公式(n-1)& hash = hash % n
  • &的运算速度快,比取模的运算速度更快,根据计算java的%,/操作比&慢10倍左右
  • 能保证索引值肯定在capacity中,不会超出数组长度。

指定数组长度不是2次幂,会破坏2次幂规则吗


不会的,HashMap的tableSizeFor方法做了处理,能保证永远保证2次幂

/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap){
   //cap-1 后,n的⼆进制最右⼀位肯定和cap的最右⼀位不同,即⼀个为0,⼀个为1,例如cap=17(00010001),n=cap-1=16(00010000)
   int n = cap -1;
   //n = (00010000 | 00001000) = 00011000
   n |= n >>>1;
   //n = (00011000 | 00000110) = 00011110
   n |= n >>>2;
   //n = (00011110 | 00000001) = 00011111
   n |= n >>>4;
   //n = (00011111 | 00000000) = 00011111
   n |= n >>>8;
   //n = (00011111 | 00000000) = 00011111
   n |= n >>>16;
   //n = 00011111 = 31
   //n = 31 + 1 = 32, 即最终的cap = 32 = 2的 (n=5)次方
   return (n <0)?1:(n >= MAXIMUM_CAPACITY)? MAXIMUM_CAPACITY : n +1;
}

HashMap源码

1.8的put方法

  1. 如果key对应的索引位置是的值null,那么直接插入
  2. 如果key对应的索引位置的数组的值不为null,判断这个老值的key和新值的key是否相同,如果相同就把老的值返回,并记录这个位置。
  3. 如果key对应的索引位置的数组的值不为null,判断这个位置的值是不是树结构,如果是树结构,调用树结构的putTreeVal方法添加数据。
  4. 如果key对应的索引位置的数组的值不为null,然后这个索引位置的值就是一个链表结构,然后遍历所有链表(当遍历长度大于8时,就会转成树结构),如果链表里的key值和新key值相同就把老的值返回,并记录这个位置。如果遍历到尾部还不相同,就是用尾插法把数据添加进去。

对2步骤和4步骤记录的位置进行处理,一是把标记位置的老值给返回,二是把新插入的值放在标记位置上面。

/**
 * Implements Map.put and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //table为空或者没有元素时,则扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果首节点的值为空,则创建一个新的首节点
    //注意(n-1)& hash才是真正的hash,也就是存储在table位置的index,
    //在1.6中是封装成indexFor函数
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//到这就说明碰撞了,就要处理碰撞
        Node<K,V> e; K k;
        //如果在首节点,与我们待插入的元素有相同的hash和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 {//到这一步说明首节点为链表类型
            for (int binCount = 0; ; ++binCount) {
                //如果遍历到末尾时,现在末尾追加该元素节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //当遍历节点数目大于8时,采取树话结构
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //如果找到与我们待插入元素具有相同的hash和key值的节点,则停止遍历,此时e已经记录了该节点
                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;
            //onlyIfAbsent表示如果当前位置已存在一个值,是否替换,false是替换,true是不替换
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);//这是一个空函数可以根据用户需要覆盖
            return oldValue;
        }
    }
    ++modCount;
    //当节点数+1大于threshold时扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);//这是一个空函数可以根据用户需要覆盖
    return null;
}

HashMap在put()时,如果put一个已经存在的key,那么会把老的key对应的值返回

1.8的get方法


  1. 首先获取当前key对应的数组索引位置,然后判断该位置的首节点是否是自己想要的值,根据key和key.hashCode()来判断
  2. 首节点如果不是的话,判断节点是否是树节点,如果是的话,调用getTreeNode()来实现get()方法,如果不是树节点,那么就是链表,然后死循环遍历链表,查询是否有自己想要的值

如果上面的步骤都没有查询到数据,直接返回null

底层源码:

public V get(Object key) {
    //定义一个node对象来接收
    Node<K,V> e;
    //调用getNode()方法,返回值赋给e,如果取得值为null,就返回null,否则就返回node对象e的value值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//取hash值方法,HahsMap的put方法也是调用了这个方法,get方法也调用了这个方法保证存取时key对应的hash值是一致的,这样才能正确对应
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final Node<K,V> getNode(int hash, Object key) {
    //定义几个变量
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //首先判断数组的table不能为空,且长度要大于0,同时把数组长度tab.length赋给n
    //其次是通过(n-1)& hash获取key对应的索引,同时数组中这个索引要有值,然后赋值给first变量
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        //这个first其实就是链表的头节点了,接下来判断first的hash值是否等于传过来key的hash值。
        // always check first node
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            //如果满足上述条件的话,说明要找的就是first节点,直接返回
            return first;
        //走到这一步,就说明要找的节点不是首节点,那就用first.next找他的后继节点,并赋值给e变量,在这个变量不为空时
        if ((e = first.next) != null) {
            //如果首节点是树类型的,那就用getTreeNode()去树里寻找
            if (first instanceof TreeNode)
                //获取树中对应key节点直接返回
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //走到这一步说明结构还是链表
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            //获取e节点的后继节点,赋值给e,不为空进入循环体
            } while ((e = e.next) != null);
        }
    }
    //以上条件都不满足说明没有该key对应的数据节点,返回null
    return null;
}

1.7HashMap的扩容原理


1.7的扩容原理,什么时候扩容和扩容多少,1.7的扩容从put方法作为入口讲解,put(K,V)操作?

public V put(K key,V value){
    if(key==null)
        return putForNullKey(value);
    int hash = hash(key);//计算key的hash值
    int i = indexFor(hash,table.length);//通过hash值对应到桶的位置
    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))){
            //注意这里的健的比较方式==或者equals
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    //遍历单链表完毕,没有找到与健相对应的Entry,需要新建一个Entry换句话说桶i是一个空桶
    addEntry(hash,key,value,i);
    return null;
}

既然找到一个空桶,那么新建的Entry必定是这个桶外挂单链表的第一个节点,通过addEntry,找到了扩容的时机

void addEntry(int hash,K key,V value,int bucketIndex){
    //size记录的是map中包含entry的含量
    //threshold记录的是需要resize的阙值,且threshold=loadFactor*capacity
    if((size >= threshold) && (null != table[bucketIndex])){
        //当size大于等于某个阙值threshold的时候且该桶并不是一个空桶
        resize(2*table.length);//将容量扩容为原来的两倍
        hash = (null!=key) ? hash(key) : 0;
        bucketIndex = indexFox(hash,table.length);//扩容后,该hash值对应的新的桶的位置
    }
    createEntry(hash,key,value,bucketIndex);//在指定桶的位置创建一个新的Entry
}


void createEntry(int hash,K key,V value,int bucketIndex){
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash,key,value,e);//链表的头插法插入新建的Entry
    //size记录的是map中包含entry的含量
    size++//更新size
}


//capacity其实就是桶的长
threshold=(int)Math.min(newCapacity*loadFactor,MAXIMUM_CAPACITY+1);

因此现在总结出扩容的时机:

当map中包含的Entry的数量大于等于threshold=loadFactor*capacity的时候,且新建的Entry刚好落在一个非空的桶上,此刻触发扩容机制,将其容量扩大为2倍。

这个这样说明比较好理解:因为size已经大于等于阙值了,说明Entry数量较多,hash冲突严重,那么若该Entry对应的桶不是一个空桶,这个Entry的加入必然会把原来的链表拉的更长因此需要扩容;若对应的桶是一个空桶,那么此时没有必要扩容。

重要

当size大于等于threshold的时候,并不一定会触发扩容机制,但是会很可能触发扩容机制,只要有一个新建的Entry出现哈希冲突,则立刻resize。

1.7HashMap的扩容过程


上面有一个很重要的方法,包含了几乎属于的扩容过程,就是resize();

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY){
        //最大容量为1<<30
        threshold = lnteger.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity]; //新建一个新表
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
    transfer(newTable, rehash);//完成旧表到新表的转移
    table = newTable;
    threshold = (int) Math.min(newCapacity * loadFactor,MAXIMUM_CAPACITY +1);
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for(Entry<K,V> e : table){
        //遍历同桶数组中的每一个桶
        while(null != e){
            //顺序遍历某个桶的外挂链表
            Entry<K,V> next = e.next; //引用next
            if (rehash){
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);//找到新表的桶位置 ;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突
            e.next = newTable[i];//头插法插入新表中
            newTable[i] = e;
            e = next;
        }
    }
}

对于resize的过程,相对来说是比较简单清晰易于理解的,旧桶数组中的某个桶的外挂单链表是通过头插法,插入新桶数组中的,并且原链表的Entry节点并不一定在新桶数组的同一链表

这很容易就想到在多线程的情况下,这个transfer方法在多线程的环境下会乱套,事实也是这样的,由于缺乏同步机制,当多个线程同时resize时,当某个线程t所持有的引用next(参考上面代码next指向源桶数组中某个桶外挂单链表下一个需要转移的Entry),可能已经被转移到了新桶数组中,那么最后该线程t实际上在对新的桶数组进行transfer操作。

如果有更多的线程出现这种情况,那很可能会出现大量线程都在对新桶数组进行transfer,这样极易造成死循环,数据丢失等等,因此HashMap不是线程安全的,所以考虑在多线程并发环境下使用并发工具包下的ConcurrentHashMap。

1.8HsahMap的扩容原理


JDK1.8对resize()方法进行了很大的调整,JDK1.8的resize()方法如下:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//定义oldCap参数,记录了table长度
    int oldThr = threshold;
    int newCap, newThr = 0;//定义了newCap参数,记录了新table长度
    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 //扩容newCap时oldCap长度的2倍
    }else if (oldThr > 0) { // initial capacity was placed in threshold
        newCap = oldThr;
    }else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        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) {
        for (int j = 0; j < oldCap; ++j) {//循环原table,把原table中的每个链表中的每个元素放入新table
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)//指的是链表中只有一个元素,所以直接吧e放入新table,其中e.hash&(newCap-1)就是计算e在新table中的位置,和JDK1.7中indexFor()作用一致
                    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;
                        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;
}

HsahMap的不安全的原因


多线程put的时候可能会造成数据丢失

put方法的源码

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) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //初始化hash表
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //通过hash值计算在hash表中的位置,并将这个位置上的元素赋值给P,如果是空的则new一个新的node放在这个位置上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //hash表的当前index已经存在元素,向这个元素后追加链表
        Node<K,V> e; K k;
        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 {
            for (int binCount = 0; ; ++binCount) {
                //新建节点并追加到链表
                if ((e = p.next) == null) {//#1
                    p.next = newNode(hash, key, value, null);//#2
                    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;
}

假设t1和t2同事执行put,假设t1执行put(k2,v2),t2执行put(k3,v3),并且k2与k3的hash值与图中的k1相同,那么正常情况下,put完成之后,table的状态应该是下图二者其一

t1,t2
t1,t2

或者

t2,t1
t2,t1

假设线程1,线程2现在都执行到put源码中的#1位置,且当前table状态如下

#1
#1

然后两个线程都执行了if((e = p.next)==null)这句代码,来到了#2这行代码

此时假设t1先执行p.next=newNode(hash,key,value,null);那么table会变成如下状态

t1
t1

紧接着t2执行p.next=newNode(hash,key,value,null);此时table会变成如下状态

t2
t2

这样一来,key2元素就丢了


多线程put和get并发的时候,可能造成get为null

线程1执行put时,因为元素隔宿超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题

先看下resize方法源码

大致的意思是,先计算新的容量和threshold,在创建一个新的hash表,最后将旧hash表中元素rehash到新的hash表中

重点代码在于#1和#2两句

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//定义oldCap参数,记录了table长度
    int oldThr = threshold;
    int newCap, newThr = 0;//定义了newCap参数,记录了新table长度
    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 //扩容newCap时oldCap长度的2倍
    }else if (oldThr > 0) { // initial capacity was placed in threshold
        newCap = oldThr;
    }else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        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];//#1
    table = newTab;//#2
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {//循环原table,把原table中的每个链表中的每个元素放入新table
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)//指的是链表中只有一个元素,所以直接吧e放入新table,其中e.hash&(newCap-1)就是计算e在新table中的位置,和JDK1.7中indexFor()作用一致
                    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;
                        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;
}

在代码#1位置,用新计算的容量new了一个新的hash表,#2将新创建的空的hash表赋值给实例变量table。

此时实例变量table是空的。

如果此时另一个线程执行get时,就会get出null

JDK1.7中HashMap因为头插法,导致get时出现死循环

JDK1.7多线程HashMap并发put的时候为何会发生环形链表,产生死循环?


HashMap扩容源码

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for(Entry<K,V> e : table){
        //遍历同桶数组中的每一个桶
        while(null != e){
            //顺序遍历某个桶的外挂链表
            Entry<K,V> next = e.next; //引用next
            if (rehash){
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);//找到新表的桶位置 ;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突
            e.next = newTable[i];//头插法插入新表中
            newTable[i] = e;
            e = next;
        }
    }
}

先创建一个散列表HashMap,Map<String,String> map = new HashMap<>(2);加载因子默认是0.75,当插入第二个元素时,会发生扩容,我们现在map中放入"itqiankun","com"两个元素,假设这两个元素的都在同一个数组的位置

put
put

这时有两个线程都执行put操作,那么在此刻两个线程都对HashMap进行扩容,这时候在源码中(关键代码)Entry<K,V> next = e.next,此时发生rehash,线程A和线程B,两个线程都会新建新的数组

假如两个线程分别是A,B两个线程。A线程在执行到关键代码这一行就被挂起,那么此刻A线程中e = "itqiankun",next = "com",接着B线程开始进行扩容,假设新的散列表中,节点"itqiankun"和节点com还是会发生散列冲突,那么B线程的扩容过程为:

将节点"itqiankun"迁移至新散列表

itqiankun
itqiankun

将com迁移至新散列表中

com
com

此时B线程的扩容已经完成,即节点com和后继节点为"itqiankun",节点"itqiankun"的后记节点为null。

此时A线程在执行到关键代码这一行处于挂起状态,那么此刻A线程中e = "itqiankun",并将next = "com"的节点赋值给e,扩容并迁移节点itqiankun后的状态,如下图:

A
A

于是在第二次执行while循环的时候,当前待处理的节点e="com"

在执行(关键代码)这一行时,

com
com

接着开始执行第三次while循环,当执行到e.next=newTable[i]的时候,此时结果为

A
A

由于节点itqiankun的后继节点为null,所以next=null,执行完第三次循环后,循环结束,此时链表成环,get()时会导致死循环。

上次编辑于:
贡献者: Jingxc,jingxc