为什么说HashMap线程不安全
作者:Cosolar 发布时间:2022-08-05 22:01:23
在Java中,HashMap是一种常用的数据结构,它以键值对的形式存储和管理数据。然而,由于HashMap在多线程环境下存在线程安全问题,因此在使用时需要格外小心。
简单来说:在 hashMap1.7 中扩容的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法; 在任意版本的 hashMap 中,如果在插入数据时多个线程命中了同一个槽,可能会有数据覆盖的情况发生,导致线程不安全。
HashMap的线程不安全主要体现在以下两个方面:
1. 并发修改导致数据不一致
HashMap的数据结构是基于数组和链表实现的。在进行插入或删除操作时,如果不同线程同时修改同一个位置的元素,就会导致数据不一致的情况。具体来说,当两个线程同时进行插入操作时,假设它们都要插入到同一个数组位置,并且该位置没有元素,那么它们都会认为该位置可以插入元素,最终就会导致其中一个线程的元素被覆盖掉。此外,在进行删除操作时,如果两个线程同时删除同一个元素,也会导致数据不一致的情况。
以下是一个示例代码,展现了两个线程对HashMap进行并发修改的情况:
import java.util.HashMap;
public class HashMapThreadUnsafeExample {
public static void main(String[] args) throws InterruptedException {
final HashMap<String, Integer> map = new HashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
map.put("key" + i, i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
map.put("key" + i, i * 2);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("map size: " + map.size());
}
}
上述示例代码中,t1线程和t2线程都向HashMap中插入数据,由于它们在进行插入操作时修改的是同一个位置的元素,因此最终导致了部分数据不一致的情况。例如,当t1线程插入了(key1, 1)以后,t2线程又插入了(key1, 2),这就导致了(key1, 1)被覆盖掉,最终HashMap的大小只有10000而不是20000。
2. 并发扩容导致死循环或数据丢失
当HashMap的元素数量达到一定阈值时,它会触发扩容操作,即重新分配更大的数组并将原来的元素重新映射到新的数组上。然而,在进行扩容操作时,如果不加锁或者加锁不正确,就可能导致死循环或者数据丢失的情况。具体来说,当两个线程同时进行扩容操作时,它们可能会同时将某个元素映射到新的数组上,从而导致该元素被覆盖掉。此外,在进行扩容操作时,如果线程不安全地修改了next指针,就可能会导致死循环的情况。
以下是一个示例代码,展现了两个线程对HashMap进行并发扩容的情况:
import java.util.HashMap;
public class HashMapThreadUnsafeExample {
public static void main(String[] args) throws InterruptedException {
final HashMap<String, Integer> map = new HashMap<>(2, 0.75f);
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
Thread t1 = new Thread(() -> {
for (int i = 4; i < 10000; i++) {
map.put("key" + i, i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 4; i < 10000; i++) {
map.put("key" + i, i * 2);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("map size: " + map.size());
}
}
上述示例代码中,t1线程和t2线程都向HashMap中插入数据,并且HashMap被初始化为大小为2,负载因子为0.75,这就意味着HashMap在元素数量达到3时就会进行扩容操作。由于t1和t2线程同时进行扩容操作,它们有可能都将某个元素映射到新的数组上,导致该元素被覆盖掉。此外,在进行扩容操作时,如果线程不安全地修改了next指针,就可能会导致死循环的情况。
除了并发修改和并发扩容外,还有以下情况可能导致HashMap不安全:
3. 非线程安全的迭代器
当使用非线程安全的迭代器遍历HashMap时,如果在遍历的过程中其他线程修改了HashMap的结构,就可能抛出ConcurrentModificationException异常。
以下是一个示例代码,展现了如何通过多线程遍历HashMap以及导致线程不安全的情况:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class HashMapThreadUnsafeExample {
public static void main(String[] args) throws InterruptedException {
final Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 10000; i++) {
map.put("key" + i, i);
}
Thread t1 = new Thread(() -> {
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next().getValue());
}
});
Thread t2 = new Thread(() -> {
for (int i = 10000; i < 20000; i++) {
map.put("key" + i, i);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
上述示例代码中,t1线程遍历了HashMap中的元素,但并没有对其进行加锁保护。同时,在t1线程遍历的过程中,t2线程又进行了另外一部分元素的插入操作,这就导致了HashMap结构的不稳定性,最终可能会抛出ConcurrentModificationException异常。
4. 非线程安全的比较器
当使用非线程安全的比较器来定义HashMap的排序规则时,就可能导致在并发环境下出现数据不一致性的情况。
以下是一个示例代码,展现了如何通过多线程修改HashMap中元素顺序以及导致线程不安全的情况:
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
public class HashMapThreadUnsafeExample {
public static void main(String[] args) throws InterruptedException {
final Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
Comparator<String> comparator = (s1, s2) -> {
int i1 = Integer.parseInt(s1.substring(3));
int i2 = Integer.parseInt(s2.substring(3));
return Integer.compare(i1, i2);
};
Thread t1 = new Thread(() -> {
for (int i = 4; i < 10000; i++) {
map.put("key" + i, i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 4; i < 10000; i++) {
map.put("key" + i, i * 2);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("map: " + map);
}
}
上述示例代码中,HashMap的排序规则使用了一个基于字符串处理的比较器来定义。当t1线程和t2线程同时进行插入操作时,由于它们在不同的元素上执行修改操作,因此并不会出现ConcurrentModificationException异常。然而,由于比较器不是线程安全的,当t1和t2线程同时进行对相同的元素值进行赋值操作时,就可能导致HashMap结构的不稳定性。例如,当t1线程将"key5"的值修改为5时,t2线程可能只修改到"value"字段的一部分,因此最终HashMap中的值可能出现混乱的情况。
写到这里我想告诉大家:HashMap在多线程环境下存在线程安全问题,具体表现为并发修改导致数据不一致和并发扩容导致死循环或数据丢失。因此,在使用HashMap时需要采取相应的线程安全措施,例如使用ConcurrentHashMap、加锁等。
来源:https://juejin.cn/post/7226207194701955130


猜你喜欢
- 本文实现初次设置密码验证过程,首先实现如下效果布局如下:<?xml version="1.0" encoding=
- 1.引言在实习期间,感受到在vs code上编程的优势(实习期间主要写的lua脚本),因此想把C++和python的开发也迁移到vs cod
- RetrofitA type-safe HTTP client for Android and Java 适用于Java和Android的安
- 一 点睛注解若想发挥更大作用,还需借助反射机制之力。通过反射,可以取得一个方法上声明的注解的全部内容。一般有两种需求:1 
- 本文实例为大家分享了Java实现中英文词典功能的具体代码,供大家参考,具体内容如下功能如下:1、可以向词典中增加中英文单词,并提供修改和删除
- 本文实例讲述了C#实现泛型List分组输出元素的方法。分享给大家供大家参考,具体如下:背景:在输出列表时,往往需要按照某一字段进行分组,比如
- 前言:来这家公司上班后,开始使用Git作为项目版本控制系统,由于以前用的是SVN,所以对Git也就简单学习了一下。但是,实践出真知,当开始使
- 第1部分 ArrayList介绍ArrayList简介ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量
- 概述MerkleTree被广泛的应用在比特币技术中,本文旨在通过代码实现一个简单的MerkleTree,并计算出Merkle tree的 T
- JDK集合源码之HashMap解析1.树结构入门1.1 什么是树?树(tree)是一种抽象数据类型(ADT),用来模拟具有树状结构性质的数据
- 一、前言TreeView这个控件对于我来说是用得比较多的,以前做的小聊天软件(好友列表)、音乐播放器(播放列表)、类库展示器(树形类结构)等
- 在谈 Volatile 之前,我们先回顾下 Java 内存模型 的三要素:原子性、可见性、有序性,也就是大家常提到的并发编程三要素。并发编程
- 本文实例讲述了C#使用iTextSharp设置PDF所有页面背景图功能的方法。分享给大家供大家参考。具体如下:在生成PDF 的时候,虽然可以
- Class类中获取方法:public Method[] getMethods();//获取包括自身和继承(实现)过来的所有的public方法
- 关于实现网易新闻客户端的界面,以前写过很多博客,请参考:Android实现网易新闻客户端效果Android实现网易新闻客户端侧滑菜单(一)A
- 本文实例为大家分享了C语言实现简单版三子棋的具体代码,供大家参考,具体内容如下游戏的主函数设计:1.打印出可以让玩家选择游戏开始和退出的菜单
- Mybatis属于半自动ORM,在使用这个框架中,工作量最大的就是书写Mapping的映射文件,由于手动书写很容易出错,我们可以利用Myba
- 一般启动一个新的Activity都默认有切换的动画效果,比如界面从右至左的移动。但是有些时候我们不需要这个动画,怎么办?操作方法比较麻烦,这
- Java中的StringUtils引入及使用pom.xml中引入依赖<!-- https://mvnrepository.com/ar
- 由其他进制转换为十进制比较简单,下面着重谈一谈十进制如何化为其他进制。1.使用Java带有的方法Integer,最简单粗暴了,代码如下//使