你好,我是风一样的树懒,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。
文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读
前一篇我们分享的《JDK 1.7 vs JDK 1.8 中 HashMap 的区别》可以看看,下一篇继续分享相关内容
HashMap 的线程不安全性主要体现在多个线程并发修改同一个 HashMap 实例时,可能会导致数据不一致、死循环、数据丢失等问题。下面是详细的体现及解释:
当多个线程同时对 HashMap 进行修改(插入、删除或更新),如果没有进行适当的同步,会导致数据的不一致性。例如,一个线程正在执行插入操作,而另一个线程正在读取该数据,可能导致读取的数据不正确,甚至出现丢失或错乱的情况。
这是 HashMap 在并发修改下非常严重的一个问题,尤其是在 JDK 1.7 版本及之前的实现中。当多个线程并发地插入元素,可能导致链表结构发生变化,链表节点可能会被重新排序,如果不采取适当的措施,可能会导致环形链表结构的形成。这种情况下,遍历该 HashMap 会进入 死循环。
在多线程并发插入操作时,HashMap 可能会丢失数据,尤其是在多线程同时执行 put() 操作时。由于 HashMap 不是线程安全的,两个线程可能会同时写入相同的 hash 值的桶中,导致其中一个线程的写入被覆盖,最终造成数据丢失。
在 HashMap 扩容时,底层数组会重新分配内存并将现有数据迁移到新数组。当多个线程同时访问 resize() 操作时,可能导致数据丢失或者结构不一致。具体来说,当两个线程同时触发扩容时,可能会出现数组的索引计算错误,导致一些数据丢失或者覆盖。
下面是一个简单的并发问题示例:
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HashMapConcurrencyExample {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, "value" + i);
}
});
executor.submit(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, "newValue" + i);
}
});
executor.shutdown();
}
}
在这个例子中,两个线程同时执行 put() 操作,将数据写入 HashMap。由于没有同步机制,可能会发生以下几种情况:
数据丢失(第二个线程可能覆盖第一个线程插入的数据)。
错误的大小或元素被丢失。
可能会引发并发问题。
读取操作 (get()) 在并发情况下,数据是可以被修改的,但读取的过程本身是无锁的。
修改操作 (put()) 并非原子操作,HashMap 在内部执行插入、删除、更新时需要多个步骤,这些步骤没有适当的同步机制来保证线程安全。
例如,put() 操作实际上是三步操作:
计算哈希值,确定存放位置。
如果目标位置已存在元素,则链接在链表或红黑树中。
更新数组位置的引用。
如果多个线程同时执行这些步骤,可能会导致其中一个线程的插入被覆盖或出错。
ConcurrentHashMap 是专门为并发设计的线程安全的 Map 实现,它通过分段锁技术(JDK 1.7 及之前)和其他优化措施(JDK 1.8 中采用了 CAS 和无锁算法)来保证并发情况下的线程安全。ConcurrentHashMap 在并发访问时,不会发生死循环、数据丢失和不一致等问题。
import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
通过 Collections.synchronizedMap() 包装 HashMap,将其转化为线程安全的 Map。但是,尽管这样可以保证对 Map 的访问是同步的,但该实现会导致性能下降,因为整个 Map 的访问会被锁住。
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>());
如果确实需要使用 HashMap,可以在使用 put()、get() 等操作时,显式地使用 synchronized 关键字,保证线程间的同步。然而,显式同步会降低并发性能,并不推荐作为常规做法。
Map<String, String> map = new HashMap<>();
synchronized (map) {
map.put("key", "value");
}
对于频繁读、少量写的场景,可以使用 ReadWriteLock 来实现优化。通过读写锁,多个线程可以并发读取,但是写操作会互斥。
HashMap 的线程不安全性体现在以下几个方面:
数据不一致、数据丢失、死循环等问题。
HashMap 在并发操作时没有同步机制,导致多线程访问时可能出错。
解决方案:
推荐使用 ConcurrentHashMap,它是线程安全的并发容器。
如果使用 HashMap,则可以通过 synchronizedMap 或手动加锁等方式保证线程安全,但性能较差。
因此,对于大多数并发场景,ConcurrentHashMap 是更加优雅和高效的解决方案。
今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!