hashmap的底层设计原理以及扩容规则,是否线程安全,如何线程安全。

HashMap的数据结构

底层使用hash表数据结构,即数组和链表或红黑树。jdk1.7使用的是 数组+链表,jdk1.8 当链表长度大于阈值(默认为8)并且数组长度达到64时 会转换为红黑树

初始容量:HashMap 的初始容量是 0,这是一种懒加载机制,直到第一次 put 操作才会初始化数组大小,默认大小是 16。

扩容逻辑

HashMap 使用的是拉链法来解决散列冲突,扩容并不是必须的,但是不扩容的话会造成拉链的长度越来越长,导致散列表的时间复杂度会倾向于 O(n) 而不是 O(1)。

HashMap 扩容的触发时机出现在元素个数超过阈值(容量 * loadFactor)的时候时,会将集合的一维数组扩大一倍,然后重新计算每个元素的位置。

  • 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  • 存储时,如果出现hash值相同的key,此时有两种情况。
    • 如果key相同,则覆盖原始值;
    • 如果key不同(出现冲突),则将当前的key-value放入链表或红黑树中
  • 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。

 

注意:链表的长度大于8 且 数组长度大于64转换为红黑树(如果数组长度小于64,即使链表长度超过 8,也不会进行树华,而是会进行扩容操作)

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都⽤到了红⿊树。红⿊树就是为了解决⼆叉查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构。

 

扩容机制 :

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是元素数量达到了扩容阈值(数组长度 * 0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍;
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    • 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
    • 如果是红黑树,走红黑树的添加
    • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

 

线程安全问题


线程不论哪个版本都是不安全的。

原因:

JDK1.7在多线程环境下,扩容引发死循环(环形链)和数据丢失在 jdk1.7 中
jdk1.7版本的HashMap线程不安全主要是发生在扩容函数中,其中调用了HshMap的transfer()方法
在执行数组扩容操作时,数据会重新定位新数组中索引,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点如何造成死循环以及数据丢失的。

JDK1.8 中,在多线程环境下,会发生数据覆盖的情况。

解决:

使用HashTable替代HashMap

当一个线程访问HashTable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以。效率很低,所以基本不用。

HashTable内方法上使用了synchronized。

类ConcurrentHashMap定义Map,ConcurrentHashMap是JUC包中的一个类,方法内部使用了synchronized保证线程安全。

Collections类的synchronizedMap(Map m)方法可以返回一个线程安全的Map

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值