jdk-HashMap-1.7-补充文章

此篇是关于初期的一篇HashMap文章的补充文章:主要涉及两个东西,一、扩容;二、扩容时的线程安全分析。

HashMap1.7

在上述篇幅里分析了hash过程,put过程和get过程。应该来说还是比较详细的。

一、扩容

扩容应该是HashMap内一个非常常见的问题。此篇还是基于1.7去补充下,1.8的稍微复杂了一些是由于引入了红黑树进去。

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

当put的时候addEntry方法内存在一个扩容的判断:

1.当size>=threshold时(通俗的讲就是当前个数是否大于阈值);

2.当前存在hash冲突了;

这里需要重点分析的是第一种情况的一些特例,比如threshold,这个值的初始值来源于下面:

threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

capacity初始默认值是16,loadFactory默认是0.75,也就是threshold默认是16*0.75=12。当个数大于12时,理论上就需要扩容了。

场景1:map中的数组初始大小是16,那么放进去的12个数据都放在了不同的数组内(假设是0-11的位置上),这样,当第13个放进来的时候(如果hash之后的位置是0-11(hash冲突了)),就需要扩容了。

场景2:map中的数组初始大小是16,那么放进去的12个数据都放在了不同的数组内(假设是0-11的位置上),这样,当第13个放进来的时候(如果hash之后的位置是12(hash没有冲突)),那么此时是不需要扩容的。这种情况下的极端例子就是16个数据在放置的时候都依次放在了16位的数组中(0-15),这样当17个数据来的时候才会扩容。

那么在最初最多能存放多少数据而不发生扩容呢?

场景3:场景3更加极端一些,初始大小是16,阈值是12,那么假设前11个值都落到了位置0上,也就是存储到了数组的同一个位置上,后续存入的15个数据都依次存放在1-15中(此时数据虽然大于阈值,但是没有发生哈市冲突,所以不扩容),当第27个数据进来时,已经没有位置了,必定发生冲突导致扩容。,所以最大的数据是11+15=26个数据

扩容后续代码

resize:

1.扩容有最大值限定,2^30方。

2.transfer就是将原数组的值放入新数组中。

3.最后重新设置threshold(新的阈值)。

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.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;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
transfer:
    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;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

transfer内没什么特殊的东西,就是重新计算hash的值在新数组中的哪一个位置上。

这里就引出了一个新的问题,关于transfer的线程安全问题,也可以说是HashMap的线程安全问题,大家都知道HashMap是线程不安全的,那么提现在哪儿呢?一个就是put的时候,另一个就是扩容里的transfer的时候。

put就不说了,比较容易理解。今天主要分析一下transfer的时候的线程安全问题;

基础前提:

1.数组初始大小为2

2.hash算法取简单的key%length 的大小。

单线程场景:


多线程场景:

多线程存在问题主要会是在哪里呢?看单线程场景中,我们可以看见对于原数组+链表的操作,存在两个指针,一个e,一个e.next。这就是问题所在(对于链表的操作指针e,如果一个线程完整操作之后,后续线程再次操作时,链表的结构已经发生改变,那么线程不安全也就无法避免)。

我们来看一看核心操作:

while(null != e) {
                Entry<K,V> next = e.next; //1
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }

问题就出现在步骤1处,假设现在存在两个线程A,线程B同时执行put操作。线程A执行到步骤1时,挂了。线程B正常执行。

因此线程A和线程B会出现下面的场景:


线程A再次被唤醒继续执行扩容:

第一次循环,此时e指向key=3的节点,e.next指向key=7的节点。因此最终的结果就是线程A的位置3指向了key=3的处于线程B中的节点。


第二次循环,注意此时e和e.next的位置变化。这个时候e指向的是key=7,对于线程A来说当前存在指向key=3的数据,因此,key=7的next指向了key=3的节点,而key=7就变成了线程A的头节点。


第三次循环,注意此时e又指向了key=3的节点,而e.next指向了null节点。如果针对key=3的节点再次操作的话,如下关键语句:

e.next = newTable[i];
key=3的next指向了第二次循环时的链表开头数据key=7。所以就形成了一个环形结构,table[3]->key[3]->key[7]->[3]。这就是在多线程下可能出现的场景。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值