热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java之HashMap在多线程情况下导致死循环的问题

PS:不得不说Java编程思想这本书是真心强大..学习内容:1.HashMap<K,V>在多线程的情况下出现的死循环现象当初学Java的时候只是知道HashMap<

PS:不得不说Java编程思想这本书是真心强大..

 

学习内容:

1.HashMap在多线程的情况下出现的死循环现象

 

  当初学Java的时候只是知道HashMap在并发的情况下使用的话,会出现线程安全问题,但是一直都没有进行深入的研究,也是最近实验室的徒弟在问起这个问题的原因之后,才开始进行了一个深入的研究.

  那么这一章也就仅仅针对这个问题来说一下,至于如何使用HashMap这个东西,也就不进行介绍了.在面对这个问题之前,我们先看一下HashMap的数据结构,学过C语言的,大家应该都知道哈希表这个东西.其实HashMap和哈希表我可以说,思想上基本都是一样的.

  这就是二者的数据结构,上面那个是C语言的数据结构,也就是哈希表,下面的则是Java中HashMap的数据结构,虽然数据结构上稍微有点差异,不过思想都是一样的.我们还是以HashMap进行讲解,我们知道HashMap有一个叫装载因子的东西,默认情况下HashMap的装载因子是75%这是在时间和空间上寻求的一个折衷.那么什么是所谓的装载因子,装载因子其实是用来判断当前的HashMap中存放的数据量,如果我们存放的数据量大于了75%,那么HashMap就需要进行扩容操作,扩容的空间大小就是原来空间的两倍.但是扩容的时候需要reshash操作,其实就是讲所有的数据重新计算HashCode,然后赋给新的HashMap,rehash的过程是非常耗费时间和空间的,因此在我们对HashMap的大小进行控制的时候,应该要进行相当的考虑.还有一个误区(HashMap可不是无限大的.)

 简单介绍完毕之后,就说一下正题吧.其实在单线程的情况下,HashMap是不会出现问题的.但是在多线程的情况下也就是并发情况下,就会出现问题.如果HashMap的容量很大,我们存入的数据很少,在并发的情况下出现问题的几率还是很小的.出现问题的主要原因就是,当我们存入的数据过多的时候,尤其是需要扩容的时候,在并发情况下是很容易出现问题.针对这个现象,我们来分析一下.

 resize()函数..

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);
//transfer函数的调用
table = newTable;
threshold
= (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

  上面说过,但HashMap的空间不足的情况下,需要进行扩容操作,因此在Java JDK中需要使用resize()函数,Android api中是找不到resize函数的,Android api是使用ensureCapacity来完成调用的..原理其实都差不多,我这里还是只说Java JDK中的..其实在resize()这个过程中,在并发情况下也是不会出现问题的..

  关键问题是transfer函数的调用过程..我们来看一下transfer的源码..

void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) { //这里才是问题出现的关键..
while(null != e) {
Entry
next = e.next; //寻找到下一个节点..
if (rehash) {
e.hash
= null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); //重新获取hashcode
e.next = newTable[i];
newTable[i]
= e;
e
= next;
}
}
}

   transfer函数其实是在并发情况下导致死循环的因素..因为这里涉及到了指针的移动的过程..transfer的源码一开始我并有完全的看懂,主要还是newTable[i]=e的这个过程有点让人难理解..其实这个过程是一个非常简单的过程..我们来看一下下面这张图片..

  这是在单线程的正常情况下,当HashMap的容量不够之后的扩容操作,将旧表中的数据赋给新表中的数据.正常情况下,就是上面图片显示的那样.新表的数据就会很正常,并且还需要说的一点就是,进行扩容操作之后,在旧表中key值相同的数据块在新表中数据块的连接方式会逆向.就拿key = 3和key = 7的两个数据块来说,在旧表中是key = 3 的数据块指向key = 7的数据块的,但是在新表中,key = 7的数据块则是指向了key = 3的数据块key = 5 的数据块不和二者发生冲突,因此就保存到了 i = 1 的位置(这里的hash算法采用 k % hash.size() 的方式).这里采用了这样简单的算法无非是帮助我们理解这个过程,当然在正常情况下算法是不可能这么简单的.

 这样在单线程的情况下就完成了扩容的操作.其中不会出现其他的问题..但是如果是在并发的情况下就不一样了.并发的情况出现问题会有很多种情况.这里我简单的说明俩种情况.我们来看图。

  这张图可能有点小,大家可以通过查看图像来放大,就能够看清晰内容了...

 这张图说明了两种死循环的情况.第一种相对而严还是很容易理解的.第二种可能有点费劲..但是有一点我们需要记住,图中t1和t2拿到的是同一个内存单元对应的数据块.而不是t1拿到了一个独立的数据块,t2拿到了一个独立的数据块..这是不对的..之所以发生系循环的原因就是因为拿到的数据块是同一个内存单元对应的数据块.这点我们需要注意..正是因为在高并发的情况下线程的工作方式是不确定的,我们无法预知线程的工作情况.因此在高并发的情况下,我们不要使用多线程对HashMap进行操作,否则我们都不知道到底是哪里出了问题.

 可能看起来很复杂,但是只要去思考,还是感觉蛮简单的,我这只是针对两个线程来分析了一下死循环的情况,当然发生死循环的问题不仅仅只是这两种方式,方式可能会有很多,我这里只是针对了两个类型进行了分析,目的是方便大家理解.发生死循环的方式绝不仅仅只是这两种情况.至于其他的情况,大家如果愿意去了解,可以自己再去研磨研磨其他的方式.按照这种思路分析,还是能研磨出来的.并且这还是两个线程,如果数据量非常大,线程的使用还比较多,那么就更容易发生死循环的现象.因此这就是导致HashMap在高并发下导致死循环的原因.

 虽然我们都知道当多线程对Map进行操作的时候,我们只需要使用ConcurrentHashMap就可以了.但是我们还是需要知道为什么HashMap在高并发的情况下不能够那样去使用.学一样东西,不仅仅要知道,而且还要知道其中的原因和道理.

 

 

 


推荐阅读
  • 本文深入解析了JDK 8中HashMap的源代码,重点探讨了put方法的工作机制及其内部参数的设定原理。HashMap允许键和值为null,但键为null的情况只能出现一次,因为null键在内部通过索引0进行存储。文章详细分析了capacity(容量)、size(大小)、loadFactor(加载因子)以及红黑树转换阈值的设定原则,帮助读者更好地理解HashMap的高效实现和性能优化策略。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • MySQL 5.7 学习指南:SQLyog 中的主键、列属性和数据类型
    本文介绍了 MySQL 5.7 中主键(Primary Key)和自增(Auto-Increment)的概念,以及如何在 SQLyog 中设置这些属性。同时,还探讨了数据类型的分类和选择,以及列属性的设置方法。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 属性类 `Properties` 是 `Hashtable` 类的子类,用于存储键值对形式的数据。该类在 Java 中广泛应用于配置文件的读取与写入,支持字符串类型的键和值。通过 `Properties` 类,开发者可以方便地进行配置信息的管理,确保应用程序的灵活性和可维护性。此外,`Properties` 类还提供了加载和保存属性文件的方法,使其在实际开发中具有较高的实用价值。 ... [详细]
  • 深入解析Java虚拟机的内存分区与管理机制
    Java虚拟机的内存分区与管理机制复杂且精细。其中,某些内存区域在虚拟机启动时即创建并持续存在,而另一些则随用户线程的生命周期动态创建和销毁。例如,每个线程都拥有一个独立的程序计数器,确保线程切换后能够准确恢复到之前的执行位置。这种设计不仅提高了多线程环境下的执行效率,还增强了系统的稳定性和可靠性。 ... [详细]
  • 本文详细介绍了 Java 中遍历 Map 对象的几种常见方法及其应用场景。首先,通过 `entrySet` 方法结合增强型 for 循环进行遍历是最常用的方式,适用于需要同时访问键和值的场景。此外,还探讨了使用 `keySet` 和 `values` 方法分别遍历键和值的技巧,以及使用迭代器(Iterator)进行更灵活的遍历操作。每种方法都附有示例代码和具体的应用实例,帮助开发者更好地理解和选择合适的遍历策略。 ... [详细]
  • 线程能否先以安全方式获取对象,再进行非安全发布? ... [详细]
  • Java中不同类型的常量池(字符串常量池、Class常量池和运行时常量池)的对比与关联分析
    在研究Java虚拟机的过程中,笔者发现存在多种类型的常量池,包括字符串常量池、Class常量池和运行时常量池。通过查阅CSDN、博客园等相关资料,对这些常量池的特性、用途及其相互关系进行了详细探讨。本文将深入分析这三种常量池的差异与联系,帮助读者更好地理解Java虚拟机的内部机制。 ... [详细]
  • Java学习第10天:深入理解Map接口及其应用 ... [详细]
  • 在使用SSH框架进行项目开发时,经常会遇到一些常见的问题。例如,在Spring配置文件中配置AOP事务声明后,进行单元测试时可能会出现“No Hibernate Session bound to thread”的错误。本文将详细探讨这一问题的原因,并提供有效的解决方案,帮助开发者顺利解决此类问题。 ... [详细]
  • 2012年9月12日优酷土豆校园招聘笔试题目解析与备考指南
    2012年9月12日,优酷土豆校园招聘笔试题目解析与备考指南。在选择题部分,有一道题目涉及中国人的血型分布情况,具体为A型30%、B型20%、O型40%、AB型10%。若需确保在随机选取的样本中,至少有一人为B型血的概率不低于90%,则需要选取的最少人数是多少?该问题不仅考察了概率统计的基本知识,还要求考生具备一定的逻辑推理能力。 ... [详细]
  • FastDFS Nginx 扩展模块的源代码解析与技术剖析
    FastDFS Nginx 扩展模块的源代码解析与技术剖析 ... [详细]
author-avatar
tomodachitch
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有