Java 基础:HashMap 假死锁问题的测试、分析和总结

本贴最后更新于 2407 天前,其中的信息可能已经渤澥桑田

前言

  前两天在公司的内部博客看到一个同事分享的线上服务挂掉 CPU100% 的文章,让我联想到 HashMap 在不恰当使用情况下的死循环问题,这里做个整理和总结,也顺便复习下 HashMap。

直接上测试代码

由于机器配置和性能不同,测试出效果的线程数和 put 数量也各不相同

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

|

public class HashMapInfiniteLoopTest {

/**

* 基于JDK1.7测试HashMap在多线程环境下假死锁的情况

* JDK1.8的HashMap实现跟1.7比较已经有很大的变化,已不存在这样的问题

* (其实这本来不是JDK的一个问题,HashMap本就不是线程安全的,多线程环境下共享一定要用线程安全的Map容器)

*/

public static void main(String[] args) {

String jdkVer = System.getProperty(``"java.version"``); ``//JDK版本

String jdkMod = System.getProperty(``"sun.arch.data.model"``); ``//32位还是64位

System.out.println(jdkVer +``"#"``+ jdkMod);

final Map map = ``new HashMap<>();

// final Map map = new ConcurrentHashMap<>();

for``(``int i=``0``; i<``30``; i++) {

new Thread(``new Runnable() {

@Override

public void run() {

System.out.println(Thread.currentThread().getName());

for``(``int j=``0``; j<``1000``; j++) {

map.put(``""``+j+``"_"``+System.currentTimeMillis(), ``""``+j+``"_"``+System.currentTimeMillis());

}

}

}, ``"myThread_"``+i).start();

}

}

}

|

  通过 jconsole 查看 Java 进程情况:

  最后只能强制结束进程

分析

  HashMap 使用 hash 表来作为其底层存储的数据结构(数组下标实现快速索引,链表实现元素碰撞处理),并且支持动态扩容,主要通过 resize 方法实现,也是从这个方法开始出问题的。(这里有两个面试官喜欢问的点:1.table 的默认长度以及扩容前后大小?2.为什么要求 table 的长度必须是 2 的 N 次方?)

  因为整个 HashMap 都不是线程安全的,所以 JDK 也未对 resize 方法做同步,如果错误的在多线程环境下共享访问了 HashMap 就有可能引起我前面提到的假死锁问题。动态扩容的时候需要把旧的链表迁移到新的 hash 表中,如果是在多线程环境下,可能会形成循环链表,在再次 put 遍历每个链表检查是否存在相同 key 时,死循环就出现了(如果是 get 也会有同样的情况)。

下面是我整理转载自 https://coolshell.cn/articles/9606.html 的部分内容(写得太好了):

|

1

2

3

4

5

6

7

8

9

10

11

12

|

void resize(``int newCapacity)

{

Entry[] oldTable = table;

int oldCapacity = oldTable.length;

......

//创建一个新的Hash Table

Entry[] newTable = new Entry[newCapacity];

//将Old Hash Table上的数据迁移到New Hash Table上

transfer(newTable);

table = newTable;

threshold = (``int``)(newCapacity * loadFactor);

}

|

迁移的源代码,注意高亮处:

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

|

void transfer(Entry[] newTable)

{

Entry[] src = table;

int newCapacity = newTable.length;

//下面这段代码的意思是:

// 从OldTable里摘一个元素出来,然后放到NewTable中

for (``int j = 0``; j < src.length; j++) {

Entry e = src[j];

if (e != null``) {

src[j] = null``;

do {

Entry next = e.next;

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];

newTable[i] = e;

e = next;

} while (e != null``);

}

}

}

|

  • 假设我们的 hash 算法就是简单的用 key mod 一下表的大小(也就是数组的长度)。

  • 最上面的是 old hash 表,其中的 Hash 表的 size=2, 所以 key = 3, 7, 5,在 mod 2 以后都冲突在 table[1]这里了。

  • 接下来的三个步骤是 Hash 表 resize 成 4,然后所有的 重新 rehash 的过程

并发下的 Rehash

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

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

|

1

2

3

4

5

6

7

|

do {

Entry next = e.next; // <--假设线程一执行到这里就被调度挂起了

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];

newTable[i] = e;

e = next;

} while (e != null``);

|

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

注意,因为 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(7)时,悲剧就出现了——Infinite Loop。

总结

  多线程并发环境下访问共享的 map 时一定要用线程安全的 Map 容器,如 ConcurrentHashMap,HashTable 等。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3201 引用 • 8217 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • TextBundle

    TextBundle 文件格式旨在应用程序之间交换 Markdown 或 Fountain 之类的纯文本文件时,提供更无缝的用户体验。

    1 引用 • 2 回帖 • 88 关注
  • SMTP

    SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP 协议属于 TCP/IP 协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。

    4 引用 • 18 回帖 • 632 关注
  • RYMCU

    RYMCU 致力于打造一个即严谨又活泼、专业又不失有趣,为数百万人服务的开源嵌入式知识学习交流平台。

    4 引用 • 6 回帖 • 61 关注
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 52 关注
  • 996
    13 引用 • 200 回帖 • 5 关注
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    29 引用 • 202 回帖 • 28 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 3 关注
  • OpenStack

    OpenStack 是一个云操作系统,通过数据中心可控制大型的计算、存储、网络等资源池。所有的管理通过前端界面管理员就可以完成,同样也可以通过 Web 接口让最终用户部署资源。

    10 引用 • 2 关注
  • 禅道

    禅道是一款国产的开源项目管理软件,她的核心管理思想基于敏捷方法 scrum,内置了产品管理和项目管理,同时又根据国内研发现状补充了测试管理、计划管理、发布管理、文档管理、事务管理等功能,在一个软件中就可以将软件研发中的需求、任务、bug、用例、计划、发布等要素有序的跟踪管理起来,完整地覆盖了项目管理的核心流程。

    10 引用 • 15 回帖 • 6 关注
  • 倾城之链
    23 引用 • 66 回帖 • 166 关注
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    268 引用 • 666 回帖 • 1 关注
  • Quicker

    Quicker 您的指尖工具箱!操作更少,收获更多!

    37 引用 • 157 回帖
  • RemNote
    2 引用 • 16 回帖 • 25 关注
  • Logseq

    Logseq 是一个隐私优先、开源的知识库工具。

    Logseq is a joyful, open-source outliner that works on top of local plain-text Markdown and Org-mode files. Use it to write, organize and share your thoughts, keep your to-do list, and build your own digital garden.

    7 引用 • 69 回帖 • 7 关注
  • 30Seconds

    📙 前端知识精选集,包含 HTML、CSS、JavaScript、React、Node、安全等方面,每天仅需 30 秒。

    • 精选常见面试题,帮助您准备下一次面试
    • 精选常见交互,帮助您拥有简洁酷炫的站点
    • 精选有用的 React 片段,帮助你获取最佳实践
    • 精选常见代码集,帮助您提高打码效率
    • 整理前端界的最新资讯,邀您一同探索新世界
    488 引用 • 384 回帖 • 5 关注
  • ZeroNet

    ZeroNet 是一个基于比特币加密技术和 BT 网络技术的去中心化的、开放开源的网络和交流系统。

    1 引用 • 21 回帖 • 651 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    284 引用 • 248 回帖
  • BAE

    百度应用引擎(Baidu App Engine)提供了 PHP、Java、Python 的执行环境,以及云存储、消息服务、云数据库等全面的云服务。它可以让开发者实现自动地部署和管理应用,并且提供动态扩容和负载均衡的运行环境,让开发者不用考虑高成本的运维工作,只需专注于业务逻辑,大大降低了开发者学习和迁移的成本。

    19 引用 • 75 回帖 • 677 关注
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    7 引用 • 28 回帖 • 2 关注
  • 锤子科技

    锤子科技(Smartisan)成立于 2012 年 5 月,是一家制造移动互联网终端设备的公司,公司的使命是用完美主义的工匠精神,打造用户体验一流的数码消费类产品(智能手机为主),改善人们的生活质量。

    4 引用 • 31 回帖 • 3 关注
  • 资讯

    资讯是用户因为及时地获得它并利用它而能够在相对短的时间内给自己带来价值的信息,资讯有时效性和地域性。

    56 引用 • 85 回帖 • 1 关注
  • 大疆创新

    深圳市大疆创新科技有限公司(DJI-Innovations,简称 DJI),成立于 2006 年,是全球领先的无人飞行器控制系统及无人机解决方案的研发和生产商,客户遍布全球 100 多个国家。通过持续的创新,大疆致力于为无人机工业、行业用户以及专业航拍应用提供性能最强、体验最佳的革命性智能飞控产品和解决方案。

    2 引用 • 14 回帖
  • 导航

    各种网址链接、内容导航。

    45 引用 • 177 回帖 • 3 关注
  • Spark

    Spark 是 UC Berkeley AMP lab 所开源的类 Hadoop MapReduce 的通用并行框架。Spark 拥有 Hadoop MapReduce 所具有的优点;但不同于 MapReduce 的是 Job 中间输出结果可以保存在内存中,从而不再需要读写 HDFS,因此 Spark 能更好地适用于数据挖掘与机器学习等需要迭代的 MapReduce 的算法。

    74 引用 • 46 回帖 • 568 关注
  • danl
    175 关注
  • WebComponents

    Web Components 是 W3C 定义的标准,它给了前端开发者扩展浏览器标签的能力,可以方便地定制可复用组件,更好的进行模块化开发,解放了前端开发者的生产力。

    1 引用 • 10 关注
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖