HashMap-深入理解同步和并发问题-线程不安全的原因 (hashmap底层实现原理)
HashMap是一种广泛使用的集合,用于在Java中存储键值对。值得注意的是,HashMap在并发环境中是线程不安全的,这意味着它不能在多线程环境中可靠地使用。
HashMap的数据结构
HashMap基于哈希表实现,它使用键的哈希码来确定该键值对应该存储在哈希表的哪个位置。HashMap的键和值是不允许重复的,这意味着对于同一个键,只能有一个值与之关联。
线程安全的含义
线程安全通常意味着在多线程环境中,多个线程能够同时访问同一个资源(如数据结构、文件等)而不会引发任何问题,例如数据损坏、不一致或非预期的行为。为了保证线程安全,通常需要通过同步机制来协调不同线程对资源的访问。
为什么HashMap是线程不安全的
HashMap是线程不安全的,原因主要有以下几个方面:并发修改导致的数据不一致
如果多个线程同时修改HashMap,可能会导致内部数据结构的不一致。例如,在扩容过程中(当HashMap中的元素数量超过其容量和负载因子的乘积时,它会进行扩容),如果有多个线程同时插入数据,可能会造成链表循环、数据丢失等问题。
快速失败迭代器
HashMap的迭代器是快速失败(fail-fast)的,这意味着在迭代过程中如果检测到结构上的任何修改,迭代器会立即抛出ConcurrentModificationException。在多线程环境中,这种异常更常见。
无同步机制
HashMap没有内置的同步机制来防止多个线程同时写入或读取时可能导致的问题。这意味着多个线程可以同时访问HashMap,从而可能导致数据损坏或不一致。
替代方案
由于HashMap是线程不安全的,因此在需要线程安全的场景中,建议使用其他数据结构,如:- ConcurrentHashMap:这是一个线程安全的HashMap实现,它使用锁来同步对底层数据结构的访问。
- Collections.synchronizedMap(Map<K, V>):这是一种包装类,它可以将任何Map实现转换为线程安全的Map。
- CopyOnWriteHashMap:这是一个线程安全的HashMap实现,它在写入操作时会创建一个新的Map副本,从而避免了并发修改问题。
在选择替代方案时,需要考虑具体场景的性能和并发性要求。对于低并发场景,可以使用Collections.synchronizedMap(Map<K, V>),因为它具有较低的开销。对于高并发场景,ConcurrentHashMap和CopyOnWriteHashMap是更好的选择,但它们的开销也更高。
结论
HashMap是一种非常有用的数据结构,但它在并发环境中是线程不安全的。因此,在需要线程安全的场景中,务必使用替代方案,如ConcurrentHashMap、Collections.synchronizedMap(Map<K, V>)或CopyOnWriteHashMap。
hashMap线程不安全的原因及表现
1 扩容时可能造成死循环,扩容时会造成死锁,形成环形链表;或者造成扩容大小不一致等问题 2 多个线程put的时,get的值可能不一致,put的操作不是原子性的 3 删除键值对的时候,会删除刚刚修改的位置元素
扩容操作时: 这个操作会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。 当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。
环形链表的原因:参考 大概看下transfer:
经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以,HashMap 的死锁问题就出在这个 transfer() 函数上。
put时: 在hashmap做put操作的时候会调用到以上的方法。现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失
删除时:参考 当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改。
补充: 内部Entry数组默认大小是16,默认负载因子是0.75
内部Entry数组大小是2的幂
元素个数超过当前大小*默认因子的时候会扩容到当前大小的2倍
扩容是为了减少单个Entry数组链表的平均长度
HashMap线程不安全的主要因素的put过程中会发生扩容,多个线程会同时操作同一块内存导致
JDK7使用数组+链表方式实现;JDK8使用数组+链表/红黑树的方式实现:链表长度为8,链表转化为红黑树;红黑树节点个数为6,红黑树会转化为链表
hashmap为什么是线程不安全的
JDK1.7中,由于多线程对HashMap进行扩容,调用了HashMap,当某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。假设两个线程A、B都在进行put操作,此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。 展示机型:华为MateBook X 系统版本:win101、JDK1.7中,由于多线程对HashMap进行扩容,调用了HashMap,当某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
2、JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。