程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 若依框架
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • Java 面经

    • Java基础
    • Java集合
    • Java并发
    • JVM面试题
    • MySQL面试题
    • Redis面试题
    • MQ面试题
    • Spring面试题
    • Mybatis面试题
    • Dubbo面试题
    • 分布式面试题
    • 操作系统面试题
    • 计算机网络面试题
    • 高频场景面试题
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 若依框架
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • Java 面经

    • Java基础
    • Java集合
    • Java并发
    • JVM面试题
    • MySQL面试题
    • Redis面试题
    • MQ面试题
    • Spring面试题
    • Mybatis面试题
    • Dubbo面试题
    • 分布式面试题
    • 操作系统面试题
    • 计算机网络面试题
    • 高频场景面试题
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Java基础

  • Java集合

    • Java集合高频面试题
    • HashMap专栏
    • HashMap的线程安全问题
      • 多线程下扩容死循环
      • 多线程的put可能导致元素的丢失
      • put和get并发时,可能导致get为null
      • 巨人的肩膀
    • ConcurrentHashMap
  • Java并发

  • JVM

  • MySQL

  • Redis

  • MQ

  • Spring

  • Mybatis

  • Dubbo

  • 分布式

  • 操作系统

  • 计算机网络

  • 高频场景题

  • Java面试题
  • Java集合
scholar
2025-01-04
目录

HashMap的线程安全问题

# HashMap的线程安全问题

我们都知道 HashMap 是线程不安全的,那 HashMap 为什么线程不安全?JDK1.8 还有这些问题吗?如何解决这些问题呢?本文将对该问题进行解密。

# 多线程下扩容死循环

JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。

下面看看多线程情况下, JDK1.7 扩容死循环问题的分析。

新建一个更大尺寸的hash表,然后把数据从老的hash表中迁移到新的hash表中。重点看下transfer方法:

假设HashMap初始化大小为2,hash算法就是用key mod 表的长度,在mod 2以后都冲突在table[1]这里了。负载因子是 1.5 (默认为 0.75 ),由公式threshold=负载因子 * hash表长度可得,threshold=1.5 * 2 =3,size=3,而 size>=threshold 就要扩容,所以 hash表要 resize 成 4。

未resize前的table如下图:

正常的ReHash,得到的结果如下图所示:

我们来看看多线程下的ReHash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组,下面是resize 的过程。

Step1:

假设 线程1 在执行到Entry<K,V> next = e.next;之后,cpu 时间片用完了,被调度挂起,这时线程1的 e 指向 节点A,线程1的 next 指向节点B。

线程2继续执行,

Step2:

线程 1 被调度回来执行。

  • 先是执行 newTalbe[i] = e;
  • 然后是e = next,导致了e指向了节点B,
  • 而下一次循环的next = e.next导致了next指向了节点A。

Step3:

线程1 接着工作。把节点B摘下来,放到newTable[i]的第一个,然后把e和next往下移。

Step4: 出现环形链表

e.next = newTable[i] 导致A.next 指向了 节点B,此时的B.next 已经指向了节点A,出现环形链表。

如果get一个在此链表中不存在的key时,就会出现死循环了。如 get(11)时,就发生了死循环。

分析见get方法的源码:

for循环中的e = e.next永远不会为空,那么,如果get一个在这个链表中不存在的key时,就会出现死循环了。

# 多线程的put可能导致元素的丢失

多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。

我们来看下JDK 1.8 中 put 方法的部分源码,重点看黄色部分:

我们来演示个例子。

假设线程1和线程2同时执行put,线程1执行put(“1”, “A”),线程2执行put(“5”, “B”),hash算法就是用key mod 表的长度,表长度为4,在mod 4 以后都冲突在table[1]这里了。注:下面的例子,只演示了 #1 和#2代码的情况,其他代码也会出现类似情况。

正常情况下,put完成后,table的状态应该是下图中的任意一个。

下面来看看异常情况,两个线程都执行了#1处的if ((p = tab[i = (n - 1) & hash]) == null)这句代码。

此时假设线程1 先执行#2处的tab[i] = newNode(hash, key, value, null);

那么table会变成如下状态:

紧接着线程2 执行tab[i] = newNode(hash, key, value, null);

此时table会变成如下状态:

这样一来,元素A就丢失了。

# put和get并发时,可能导致get为null

线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。

我们来看下JDK 1.8 中 resize 方法的部分源码,重点看黄色部分:

在代码#1位置,用新计算的容量new了一个新的hash表,#2将新创建的空hash表赋值给实例变量table。

注意此时实例变量table是空的,如果此时另一个线程执行get,就会get出null。

# 巨人的肩膀

http://mailinator.blogspot.com/2009/06/beautiful-race-condition.html

https://coolshell.cn/articles/9606.html

https://juejin.cn/post/6844903554264596487

https://juejin.cn/post/6844903796225605640

编辑此页 (opens new window)
上次更新: 2025/01/05, 02:09:04
HashMap专栏
ConcurrentHashMap

← HashMap专栏 ConcurrentHashMap→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式