前言:

我们都知道,HashMap在并发环境下使用可能出现问题,但是具体表现,以及为什么出现并发问题,

可能并不是所有人都了解,这篇文章记录一下HashMap在多线程环境下可能出现的问题以及如何避免。

在分析HashMap的并发问题前,先简单了解HashMap的put和get基本操作是如何实现的。

1.HashMap的put和get操作

大家知道HashMap内部实现是通过拉链法解决哈希冲突的,也就是通过链表的结构保存散列到同一数组位置的两个值,

put操作主要是判空,对key的hashcode执行一次HashMap自己的哈希函数,得到bucketindex位置,还有对重复key的覆盖操作。

对照源码分析一下具体的put操作是如何完成的:

涉及到的几个方法:

static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4);}static int indexFor(int h, int length) { return h & (length-1);}

数据put完成以后,就是如何get,我们看一下get函数中的操作:

看一下链表的结点数据结构,保存了四个字段,包括key,value,key对应的hash值以及链表的下一个节点:

static class Entry implements Map.Entry { final K key;//Key-value结构的key V value;//存储值 Entry next;//指向下一个链表节点 final int hash;//哈希值 }

2.Rehash/再散列扩展内部数组长度

哈希表结构是结合了数组和链表的优点,在最好情况下,查找和插入都维持了一个较小的时间复杂度O(1),

不过结合HashMap的实现,考虑下面的情况,如果内部Entry[] tablet的容量很小,或者直接极端化为table长度为1的场景,那么全部的数据元素都会产生碰撞,

这时候的哈希表成为一条单链表,查找和添加的时间复杂度变为O(N),失去了哈希表的意义。

所以哈希表的操作中,内部数组的大小非常重要,必须保持一个平衡的数字,使得哈希碰撞不会太频繁,同时占用空间不会过大。

这就需要在哈希表使用的过程中不断的对table容量进行调整,看一下put操作中的addEntry()方法:

void addEntry(int hash, K key, V value, int bucketIndex) { Entry e = table[bucketIndex]; table[bucketIndex] = new Entry(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }

这里面resize的过程,就是再散列调整table大小的过程,默认是当前table容量的两倍。

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]; //初始化一个大小为oldTable容量两倍的新数组newTable transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor);}

关键的一步操作是transfer(newTable),这个操作会把当前Entry[] table数组的全部元素转移到新的table中,

这个transfer的过程在并发环境下会发生错误,导致数组链表中的链表形成循环链表,在后面的get操作时e = e.next操作无限循环,Infinite Loop出现。

下面具体分析HashMap的并发问题的表现以及如何出现的。

3.HashMap在多线程put后可能导致get无限循环

HashMap在并发环境下多线程put后可能导致get死循环,具体表现为CPU使用率100%,

看一下transfer的过程:

这里引用酷壳陈皓的博文


并发下的Rehash

1)假设我们有两个线程。我用红色和浅蓝色标注了一下。

我们再回头看一下我们的 transfer代码中的这个细节:

而我们的线程二执行完成了。于是我们有下面的这个样子。

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

2)线程一被调度回来执行。

  • 先是执行 newTalbe[i] = e;
  • 然后是e = next,导致了e指向了key(7),
  • 而下一次循环的next = e.next导致了next指向了key(3)

3)一切安好。

线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移

4)环形链接出现。

e.next = newTable[i] 导致 key(3).next 指向了 key(7)

注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop


针对上面的分析模拟这个例子,

这里在run中执行了一个自增操作,i++非原子操作,使用AtomicInteger避免可能出现的问题:

public static void main(String[] args){ MapThread t0 = new MapThread(); MapThread t1 = new MapThread(); // 省略 t2-t9 t0.start(); t1.start(); // 省略 t2-t9}

注意并发问题并不是一定会产生,可以多执行几次,

我试验了上面的代码很容易产生无限循环,控制台不能终止,有线程始终在执行中,

这是其中一个死循环的控制台截图,可以看到六个线程顺利完成了put工作后销毁,还有四个线程没有输出,卡在了put阶段,感兴趣的可以断点进去看一下:

上面的代码,如果把注释打开,换用ConcurrentHashMap就不会出现类似的问题。

4.多线程put的时候可能导致元素丢失

HashMap另外一个并发可能出现的问题是,可能产生元素丢失的现象。

考虑在多线程下put操作时,执行addEntry(hash, key, value, i),如果有产生哈希碰撞,

导致两个线程得到同样的bucketIndex去存储,就可能会出现覆盖丢失的情况:

5.使用线程安全的哈希表容器

那么如何使用线程安全的哈希表结构呢,这里列出了几条建议:

使用Hashtable 类,Hashtable 是线程安全的;

使用并发包下的java.util.concurrent.ConcurrentHashMap,ConcurrentHashMap实现了更高级的线程安全;

或者使用synchronizedMap() 同步方法包装 HashMap object,得到线程安全的Map,并在此Map上进行操作

小编在学习过程中整理了一些学习资料,可以分享给做java的工程师朋友们,相互交流学习,需要的可以私信(资料)即可免费获取Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

其中覆盖了互联网的方方面面,期间碰到各种产品各种场景下的各种问题,很值得大家借鉴和学习,扩展自己的技术广度和知识面。最后记得帮作者点个关注

hashmap put复杂度_你碰到过几种HashMap在高并发下出现的问题,哪些可能出现的问题...相关推荐

  1. hashmap put复杂度_集合类HashMap,HashTable,ConcurrentHashMap区别?

    作者简介: 华哥 10年+后端开发工作经验, 主要分享:关于java体系的知识,如:java基础知识/数据结算/算法,Spring/MyBatis/Netty源码分析,高并发/高性能/分布式/微服务架 ...

  2. python 线程安全链表_教你用 Python 实现 HashMap 数据结构

    <犬夜叉·镜中的梦幻城> 今天这篇文章给大家讲讲hashmap,这个号称是所有Java工程师都会的数据结构.为什么说是所有Java工程师都会呢,因为很简单,他们不会这个找不到工作.几乎所有 ...

  3. hashmap value占用空间大小_【Java集合框架002】原理层面:HashMap全解析

    一.前言 二.HashMap 2.1 HashMap数据结构 + HashMap线程不安全 + 哈希冲突 2.1.1 HashMap数据结构 学习的时候,先整体后细节,HashMap整体结构是 底层数 ...

  4. java文字旋转90度_菜鸟求助:在 JAVA APPLET 里如何旋转 90 度显示一个字符?

    | import java.awt.*; import java.awt.event.*; import javax.swing.*; class TextRotatePanel extends JP ...

  5. hashmap原理_想要彻底搞懂HashMap?你得恶补下HashMap原理

    引言 唉! 金九银十转眼已过, 面试现场不知所措: 不懂原理是我过错, 今日弥补只为求过. ====================================================== ...

  6. hashmap底层原理_周末自己动手撸一个 HashMap,美滋滋

    对HashMap的思考 通过写一个迷你版的HashMap来深刻理解 定义接口 接口实现 看MyHashMap的构造 Entry 看put如何实现 hash函数 resize和rehash get实现 ...

  7. python余弦相似度_推荐系统01--余弦相似度

    今天,我们来聊聊协同过滤中的相似度计算方法有哪些. 相似度的本质 推荐系统中,推荐算法分为两个门派,一个是机器学习派,另一个就是相似度门派.机器学习派是后起之秀,而相似度派则是泰山北斗,以致撑起来推荐 ...

  8. python根据词向量计算相似度_基于词向量的词语间离和句子相似度分析

    基于词向量的词语间离和句子相似度分析 苟瀚文 1 苟先太 2 [摘 要] 分析了词向量在自然语言处理中的作用.使用已经训练好的词向量进 行了简单类比推理.词语间离和句子相似度分析.给出一种结合词向量和 ...

  9. mysql lbs 附近的人_一口气说出 4种 LBS “附近的人” 实现方式,面试官笑了

    引言 昨天一位公众号粉丝和我讨论了一道面试题,个人觉得比较有意义,这里整理了一下分享给大家,愿小伙伴们面试路上少踩坑.面试题目比较简单:"让你实现一个附近的人功能,你有什么方案?" ...

最新文章

  1. (C++)1021 个位数统计
  2. sudo自动键入密码
  3. Python之compiler:compiler库的简介、安装、使用方法之详细攻略
  4. python 标准差_标准差python
  5. 某著名公司2015暑期实习招聘试题及相关内容复习
  6. 谈谈低代码趋势和开发人员的未来
  7. c语言中memcpy函数_带有示例的C中的memcpy()函数
  8. android系统自动休眠代码流程,Android P 电源管理(4)待机流程
  9. ZLYD团队第一周项目总结
  10. socket编程:SO_REUSEADDR例解
  11. 《System语言详解》——3. SystemTap脚本的各大组件
  12. C# 16进制转10进制 16进制字符串转换
  13. java cropper_layui剪裁插件cropper一个页面调用多次问题解决
  14. 第11章-ThreadSpecificStorage
  15. ITSM基础框架开发维护指南
  16. 华3交换机3层vlan隔离配置
  17. 【期望】几何分布的期望
  18. html5/css实现字体上划线
  19. 如何生成git的公钥和私钥
  20. 云计算技术 实验三 安装Hadoop系统并熟悉hadoop命令

热门文章

  1. 情商加油站:职业经理人的10大自我修炼工具
  2. vs2015professional过期后登录微软账户仍然不能使用的解决方法
  3. IE6下使用jquery.bgiframe.js插件解决下拉框覆盖浮动层、Dialog的问题
  4. 基于Spring Security的AJAX请求需要登录的解决方案
  5. 【干货】美团大脑系列之商品知识图谱的构建及应用.pdf(附下载链接)
  6. 【报告分享】2020直播电商行业研究报告.pdf(附下载链接)
  7. 1024程序员节:心疼被段子手黑得最惨的你们
  8. 如何自己找数据分析项目来做?
  9. ubuntu apt-get 时遇到waiting for headers的破解办法
  10. 深度学习数据预处理——批标准化(Batch Normalization)