为什么说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
猜你喜欢
- 译文链接: https://www.infoworld.com/art...AutoMapper 是一个非常流行的 object-to-ob
- 前言服务消费者调用服务提供者的时候使用RestTemplate技术存在不便之处:拼接urlrestTmplate.getForObJect这
- 要讲到C#源码的执行过程 首先要提下程序集,因为Clr并不是和托管摸块打交道的,而是和程序集(dll,exe)1、从哪里来程序集是由一个或者
- 1.map遍历快速实现边距,文字自适应改变大小Container( // padding: EdgeI
- 安卓和苹果的客户端开发中,经常会使用到webview,我们一般做法是将webview加入到native页面中。当我们对页面进行销毁的时候,其
- 本文实例讲述了C#不重复输出一个数组中所有元素的方法。分享给大家供大家参考。具体如下:1.算法描述0)输入合法性校验1)建立临时数组:与原数
- 先新建一个文件夹kun,kun就是类所在的package。新建一个java文件。HelloWorld.java的代码如下:package k
- 一,FileWritter写入文件FileWritter, 字符流写入字符到文件。默认情况下,它会使用新的内容取代所有现有的内容,然而,当指
- 一、流程和任务的关系以下是一个简单的请假流程图,其中有一个开始事件,两个用户任务,一个结束事件。启动流程后,activiti会自动创建第一个
- 使用 fragmentLayout 实现,可以把小红点添加到任意 view 上。效果 添加小红点到 textview 上添加小红点到 ima
- 前言上一篇已经对线程池的创建进行了分析,了解线程池既有预设的模板,也提供多种参数支撑灵活的定制。本文将会围绕线程池的生命周期,分析线程池执行
- static 表示静态,它可以修饰属性,方法和代码块。1.static修饰属性(类变量),那么这个属性就可以用类名.属性名来访问,也就是使这
- 在定义一个Rest接口时通常会利用GET、POST、PUT、DELETE来实现数据的增删改查;这几种方式有的需要传递参数,后台开发人员必须对
- 一.MyBatis简介 一说起对象关系映射框架,大家第一时间想到的肯定是Hibernate。Hibern
- 本文介绍spring中自定义缓存resolver,通过自定义resolver,可以在spring的cache注解中增加附加处理。一、概述ca
- 目录创建线程管理线程销毁线程创建线程线程是通过扩展 Thread 类创建的。扩展的 Thread 类调用 Start() 方法来开
- 已知一副扑克牌有54张,去除大王和小王,剩余52张。在其中随机抽取4张牌,利用加减乘除进行计算得到24. 从A到10,他们的值分别为1到10
- 文件比较平常都是用Beyond Compare,可以说离不开的神器,特别是针对代码比较这块,确实挺好用的。不过Beyond Compare平
- import java.util.Arrays;/** * 栈的实现<br> * @author Skip&
- springboots使用的版本是2.0.1,注意不同版本可能有差异,并不一定通用添加Mybatis的起步依赖:<!--mybatis