京东一面:为什么 HashMap 是线程不安全的?

// 同一位置上的新元素被放在链表的头部

e.next = newTable[i];

// 放在新的数组上

newTable[i] = e;

// 链表上的下一个元素

e = next;

}

}

}

注意 e.next = newTable[i]newTable[i] = e 这两行代码,就会将同一位置上的新元素被放在链表的头部。

扩容前的样子假如是下面这样子。

那么正常扩容后就是下面这样子。

假设现在有两个线程同时进行扩容,线程 A 在执行到 newTable[i] = e; 被挂起,此时线程 A 中:e=3、next=7、e.next=null

线程 B 开始执行,并且完成了数据转移。

此时,7 的 next 为 3,3 的 next 为 null。

随后线程A获得CPU时间片继续执行 newTable[i] = e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

执行下一轮循环,此时 e=7,原本线程 A 中 7 的 next 为 5,但由于 table 是线程 A 和线程 B 共享的,而线程 B 顺利执行完后,7 的 next 变成了 3,那么此时线程 A 中,7 的 next 也为 3 了。

采用头部插入的方式,变成了下面这样子:

好像也没什么问题,此时 next = 3,e = 3。

进行下一轮循环,但此时,由于线程 B 将 3 的 next 变为了 null,所以此轮循环应该是最后一轮了。

接下来当执行完 e.next=newTable[i] 即 3.next=7 后,3 和 7 之间就相互链接了,执行完 newTable[i]=e 后,3 被头插法重新插入到链表中,执行结果如下图所示:

套娃开始,元素 5 也就成了弃婴,惨~~~

不过,JDK 8 时已经修复了这个问题,扩容时会保持链表原来的顺序,参照HashMap 扩容机制的这一篇。

02、多线程下 put 会导致元素丢失

正常情况下,当发生哈希冲突时,HashMap 是这样的:

但多线程同时执行 put 操作时,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。

put 的源码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

// 步骤①:tab为空则创建

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

// 步骤②:计算index,并对null做处理

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

else {

Node<K,V> e; K k;

// 步骤③:节点key存在,直接覆盖value

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;

}

// key已经存在直接覆盖value

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;

}

问题发生在步骤 ② 这里:

if ((p = tab[i = (n - 1) & hash]) == null)

tab[i] = newNode(hash, key, value, null);

两个线程都执行了 if 语句,假设线程 A 先执行了 tab[i] = newNode(hash, key, value, null),那 table 是这样的:

接着,线程 B 执行了 tab[i] = newNode(hash, key, value, null),那 table 是这样的:

3 被干掉了。

03、put 和 get 并发时会导致 get 到 null

线程 A 执行put时,因为元素个数超出阈值而出现扩容,线程B 此时执行get,有可能导致这个问题。

注意来看 resize 源码:

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) {

// 超过最大值就不再扩充了,就只好随你碰撞去吧

if (oldCap >= MAXIMUM_CAPACITY) {

threshold = Integer.MAX_VALUE;

return oldTab;

}

// 没超过最大值,就扩充为原来的2倍

else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

oldCap >= DEFAULT_INITIAL_CAPACITY)

newThr = oldThr << 1; // double threshold
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

经过日积月累, 以下是小编归纳整理的深入了解Java虚拟机文档,希望可以帮助大家过关斩将顺利通过面试。
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。







由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
OJ-1713463917308)]
[外链图片转存中…(img-sXgXCCha-1713463917308)]
[外链图片转存中…(img-Lu3NwTNY-1713463917308)]
[外链图片转存中…(img-1saeDrzo-1713463917309)]
[外链图片转存中…(img-WHQCcwEn-1713463917309)]

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值