多线程使用HashMap会导致什么问题

1 HashMap中的关键属性

capacity:hash表桶的数量
size:hash表中Entry<K,V>的数量
DEFAULT_LOAD_FACTOR=0.75f 默认装载因子的大小,也就是size/capacity大于它的时候就要进行扩容;如果为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数,选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择;
DEFAULT_INITIAL_CAPACITY = 1 << 4; 默认初始容量-必须为2的幂
UNTREEIFY_THRESHOLD = 6 桶里面的数据个数小于等于6时,由红黑树变为链表,在resize方法中使用
(为什么是6,防止出现元素长度在8-7之间变化导致红黑树和链表频繁转换的情况)
TREEIFY_THRESHOLD = 8 桶里面的数据大于8时,由链表变为红黑树,从代码可以看出,当链表已经有8个节点了,此时再新链上第9个节点,在成功添加了这个新节点之后,立马做链表转红黑树
(1 此时链表的查询平均时间复杂度时O(4),大于log2_8;2理想情况下使用随机的哈希码,容器中节点分布在hash桶中的频率遵循泊松分布(具体可以查http://en.wikipedia.org/wiki/Poisson_distribution),按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。)
MIN_TREEIFY_CAPACITY = 64 桶的数量必须大于等于64时才会将链表转换为红黑树
最大不能超过MAXIMUM_CAPACITY = 1<<30(230)

2 多线程使用HashMap会导致的问题

1 线程1put()时,记录了头结点为node1,这时时间片用完,线程2put(),且把数据插在了链表的头部,完成put操作。线程1接着完成put()剩余的操作,这时新的头结点已经变了,但是线程1记录的旧的头结点,把数据插入到头结点,覆盖了线程2put的数据,导致结果错误。
2 两个线程同时扩容会导致循环链表
注意下面(JDK1.7源码)代码中的 1,2三个步骤,这是产生死循环的关键
比如说桶里有两个结点A->B,线程1put时扩容,走到代码第一个位置前时,时间片用完,线程2介入,调用put也进行了扩容,此时结点书序时B->A,线程2结束线程1继续执行,执行1,2两个步骤,结果A.next=B,由于此时B.next=A,这就产生了死循环

    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];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     //for循环中的代码,逐个遍历链表,重新计算索引位置,将老数组数据复制到新数组中去(数组不存储实际数据,所以仅仅是拷贝引用而已)
        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);
          //将当前entry的next链指向新的索引位置,newTable[i]有可能为空,有可能也是个entry链,如果是entry链,直接在链表头部插入。
                e.next = newTable[i];//1
                newTable[i] = e;//2
                e = next;
            }
        }
    }

图片来自https://www.cnblogs.com/tilamisu007/p/9438356.html
图片来自https://www.cnblogs.com/tilamisu007/p/9438356.html

3 java1.7中的HashMap和java1.8有什么区别

1.7采用数组+单链表,1.8在单链表超过一定长度后改成红黑树存储
1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
table中的元素只有两种情况:
元素hash值第N+1(因为是hash值和数组长度-1的&操作)位为0:不需要进行位置调整
元素hash值第N+1位为1:调整至原索引+oldCap(也就是现在的size/2)位置
扩容或初始化完成后,resize方法返回新的table
1.7插入元素到单链表中采用头插入法,1.8采用的是尾插入法
https://www.cnblogs.com/LiaHon/p/11149644.html
关于代码的详细理解请参见:
https://www.cnblogs.com/heyonggang/p/9899908.html
https://blog.csdn.net/u012156116/article/details/81206649

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值