HashMap-线程不安全的原因
前言
HashMap线程安全的问题,在各大面试中都会被问到,属于常考热点题目。虽然大部分读者都了解它不是线程安全的,但是再深入一些,问它为什么不是线程安全的,仔细说说原理,用图画出一种非线程安全的情况?1.8之后又做了什么改善了这点?很多读者可能一时想不出很好的答案。
其实在网上已经有很多优秀的博主讨论过这个问题,本文的写作意图是通过更加详细的画图分析和1.7与1.8之间版本对比来帮助大家通过java面试。
1.7版本的HashMap
我们关注下面的代码
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
这段代码的主要做的是rehash,其中的核心代码是12~16行,下面画图分析,当有多个线程同时做rehash时,会发生什么。
假设条件如下:
扩容前数据长度为2,扩容后为4,并且有key为3,5,7的三个entry需要rehash
有两个线程都在做rehash,第一个线程执行到12行挂起,第二个线程rehash结束
扩容前的图示如下:
线程2执行完成rehash和线程1执行到12挂起的图示如下:
然后我们一步一步的分析线程1继续rehash的情况
当e为key3时,经过12~16步后的图示如下,我们发现3被插入到了头部,并且形成了环形链表
因为e不为null,所以我们继续执行12~16行,执行完毕后如图所示
至此两个线程的扩容都完毕,形成了环形链表。
所以当调用get方法时,因为环形链表的存在,形成一个死循环,占满cpu。
1.8版本的HashMap
首先说明,1.8版本的resize方法做了一些优化,优化的点主要在于,当hashmap的size扩容为2倍时,其实不需要每个元素都计算hash值,元素在新数组中的位置只有以下两种情况:
- 原位置
- 原位置+原数组长度
为什么只有这两种情况呢?我们看下图:
a是原数组长度-1,b是扩容为2倍的新数组长度-1,对于第一行n-1来说,其实就多了从右往左的第五位的1。
对于key1和key2来说,key1的第五位是0,key2的第五位是1,所以rehash后:
- key1的元素位置仍为原来的位置不变 仍为5
- key2的元素位置为 原来位置 + 原数组长度 5+16 = 21
明白这点后,我们继续关注下面的代码,因为resize方法中还有一些和问题领域不那么相关的代码,所以我只粘贴出分析问题的必需代码。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//原数组遍历,拿到链表头
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//关注这里的rehash代码
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//代表了多出来的第X位为0的情况
if ((e.hash & oldCap) == 0) {
if (loTail == null)
//没有元素时,设置头部为e
loHead = e;
else
//有元素时,把e插入到尾部
loTail.next = e;
//更新链表尾部指针到最新结点
loTail = e;
}
//代表了多出来的第X位为1的情况
else {
if (hiTail == null)
//没有元素时,设置头部为e
hiHead = e;
else
//有元素时,把e插入到尾部
hiTail.next = e;
//更新链表尾部指针到最新结点
hiTail = e;
}
} while ((e = next) != null);
//走到这里,就已经把原链表结点分成了两组
//设置位置不变组的数组头结点为loHead结点
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//设置新数组的头结点为loHead结点
//设置位置变为原位置 + 原数组长度 组的数组头结点为hiHead结点
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
多线程分析
我们依旧按照上面的假设画图分析
假设条件如下:
扩容前数据长度为2,扩容后为4,并且有key为3,5,7的三个entry需要rehash
有两个线程都在做rehash,第一个线程执行到19行挂起,第二个线程rehash结束
因为1.8版本的插入过程修改为了尾插法,插入前的图示变为如下所示
线程2执行完成rehash和线程1执行到19行挂起时状态如下:
线程1继续rehash,走完7和3之后的结果如下
这里我们可以看到,并没有形成环形链表,所以使用尾插法解决了1.7版本在文中分析情况下的环形链表问题。
总结
通过上文的分析,我们解决了前言中提出的两个问题。
- 我们画图分析出1.7版本的形成环形链表的具体情况
- 了解了1.8版本使用的resize的尾插法,可以解决环形链表问题
- 学习了1.8版本针对resize的优化策略
HashMap-线程不安全的原因的更多相关文章
- 谈谈HashMap线程不安全的体现
原文出处: Hosee HashMap的原理以及如何实现,之前在JDK7与JDK8中HashMap的实现中已经说明了. 那么,为什么说HashMap是线程不安全的呢?它在多线程环境下,会发生什么情况呢 ...
- java线程数过高原因分析
作者:鹿丸不会多项式 出处:http://www.cnblogs.com/hechao123 转载请先与我联系. 一.问题描述 前阵子我们因为B机房故障,将所有的流量切到了A机房,在经历了推送+ ...
- HashMap的实现原理?如何保证HashMap线程安全?
A:HashMap简单说就是它根据建的hashcode值存储数据的,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历的顺序是不确定的. B:HashMap基于哈希表,底层结构由数组来实 ...
- HashMap闭环(死循环)的详细原因(转)
为何出现死循环简要说明 HashMap是非线程安全的,在并发场景中如果不保持足够的同步,就有可能在执行HashMap.get时进入死循环,将CPU的消耗到100%. HashMap采用链表解决Hash ...
- (转载)两种方法让HashMap线程安全
HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全. 方法一:通过Collections.synchronizedMap() ...
- HashMap线程不安全的体现
前言:我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密. 1.jdk1.7中的HashMap 在jdk1.8中对HashM ...
- HashMap&线程
1.HashMap概念 HashMap是一个散列表,存储内容是键值对(key-value)的映射, HashMap继承了AbstractMap,实现了Map.Cloneable.java.io.Ser ...
- struts2的action是线程安全的,struts1的action不是线程安全的真正原因
为什么struts2的action是线程安全的,struts1的action不是线程安全的? 先对struts1和struts2的原理做一个简单的讲解 对于struts1 ,当第一次**.do的请求过 ...
- 为什么HashMap线程不安全,Hashtable和ConcurrentHashMap线程安全
HashMap源码 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final ...
- UI的线程问题:单线程原因及更新UI的四种方式
1.UI线程为什么设计为单线程? UI控件的操作不是线程安全的,对于多线程并发访问的时候,如果使用加锁机制会导致: UI控件的操作变得很复杂. 加锁的操作必定会导致效率下降. 所以android系统在 ...
随机推荐
- 【Go】彩云小译翻译接口js逆向解密返回值
一.前言 对彩云小译网页版进行抓包分析,将js算法代码转换成go代码,使用go发送http请求编写一个翻译小工具. 主要实现: 翻译(解密翻译结果) 单词字典查询 生成JWT(保持有效期) 二.抓包 ...
- 深入解析子查询(SUBQUERY):增强 SQL 查询灵活性的强大工具
title: 深入解析子查询(SUBQUERY):增强 SQL 查询灵活性的强大工具 date: 2025/1/12 updated: 2025/1/12 author: cmdragon excer ...
- ffmpeg简易播放器(1)--了解视频格式
视频帧 对于一份视频,实质上是多张图片高速播放形成的.每一张图片即为该视频的一帧.而每秒钟播放的图片张数便为所谓的帧率(Frame Rate/Frame Per Second).常见的帧率有24fps ...
- google gtest框架入门使用案例
通过本文可以收获:google gtest急速入门.google gtest资源网站. google gtest是什么 google gtest是谷歌开源的c++单元测试框架,非常的好用. 起码个人感 ...
- 在SOUI中使用GetAttribute获取XML中配置的属性
SOUI的界面布局使用XML来布局,在XML配置的属性值会通过SObject::SetAttribute(name, value)的方式交给SObject处理. SWindow是SOUI的界面元素对象 ...
- Web安全知识记录
本文分享自天翼云开发者社区<Web安全知识记录>,作者:赵****雅 1.网站置于服务器中,而服务器则是指连接在网络中的一台计算机.当我们浏览网站时,实际上就是我们用个人计算机通过网络访问 ...
- 穿越周期!天翼云laaS+PaaS全年市场份额跃居中国公有云市场第三!
近日,国际数据公司(IDC)发布<中国公有云服务市场(2023下半年)跟踪>报告.中国电信天翼云在市场进入战略调整期的背景下,2023年H2实现公有云IaaS市场份额增长至12.9%,位居 ...
- 基于Java语言的开源能管平台才是最适合国内的能源管理平台
在"双碳"战略背景下,能源管理已成为政府.企业实现可持续发展的必经之路.面对市场上各类能源管理平台,为何基于Java语言的开源解决方案才是最佳选择?本文将为您揭晓答案,并向您推荐我 ...
- macOS的PrivilegedHelperTools文件夹
在 macOS 上,/Library/PrivilegedHelperTools 文件夹是一个特殊的位置,用于存放可以以更高权限运行的辅助工具或守护进程.这些工具通常由各种应用程序安装,以执行需要超出 ...
- Scratch教程:第1课认识Scratch
一.Scratch来源 Scratch最初是由麻省理工学院媒体实验室的终身幼儿园研究小组的米切尔雷斯尼克和西摩于2003年发起的. 希望帮助所有的孩子"发现和跟随自己的激情和探索力,敢于尝试 ...