HashMap 线程不安全体现在哪里?如果你到现在还不清楚赶紧看下去,明明白白补一补~
在 Java 中,HashMap 是一种常用的数据结构,它以键值对的形式存储和管理数据。然而,由于 HashMap 在多线程环境下存在线程安全问题,因此在使用时需要格外小心。
简单来说:在 hashMap1.7 中扩容的时候,因为采用的是头插法,所以会可能会有循环链表产生,导致数据有问题,在 1.8 版本已修复,改为了尾插法;在任意版本的 hashMap 中,如果在插入数据时多个线程命中了同一个槽,可能会有数据覆盖的情况发生,导致线程不安全。
HashMap 的线程不安全主要体现在以下两个方面:
一、并发修改导致数据不一致
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。
二、并发扩容导致死循环或数据丢失
当 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 不安全:
三、非线程安全的迭代器
当使用非线程安全的迭代器遍历 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 异常。
四、非线程安全的比较器
当使用非线程安全的比较器来定义 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、加锁等。
作者:Cosolar
链接:https://juejin.cn/post/7226207194701955130
来源:稀土掘金
评论