HashMap
HashMap是基于哈希表的Map接口实现,是非线程安全的。
JDK1.8之前 HashMap 底层是数组+链表实现的,数组是HashMap的主体,链表则是为了解决哈希冲突的。(拉链法)
JDK1.8之后 HashMap 在解决哈希冲突的方法有了较大的变化,当链表长度大于等于8并且数组长度大于等于64后,会将链表转化为红黑树,以减少搜索时间。
说一下HashMap的实现原理
HashMap的数据结构是数组+链表(JDK 1.7及之前) or 链表&红黑树(JDK 1.8之后)
- 我们往HashMap里put元素时,利用key的哈希值(hashcode()算出来的)重新hash计算出当前元素在数组中的下标。
- 若key相同则覆盖
- 若key不同则存入链表或红黑树中
获取数据时,计算key的hash值得到对应的下标,如果里面有多个Entry的话,我们需要进一步遍历这些 Entry 来找到与给定 key 相匹配的 Entry,从而找到对应值。
说一下HashMap的put方法的具体流程
1.判断存储键值对的数组 table 是否为 null ,如果是 null 的话就执行初始化扩容操作,把数组长度设置为16,扩容阈值设为12
2.根据 key 计算 hash 值得到数组下标索引 i
3.判断 table[i] 是否等于 null ,如果等于 null ,就直接新建节点添加
4.如果 table[i] 不等于 null
- 4.1 判断 table[i] 的首个元素是否等于key,如果相同就直接覆盖value
- 4.2 判断 table[i] 是否是红黑树,如果是红黑树就直接在树中插入键值对,插入过程中如果发现 key 已存在就直接覆盖 value
- 4.3 若不是红黑树,就是链表,在链表尾部插入数据,如果遍历过程中发现 key 已存在就直接覆盖value ,然后判断链表长度是否大于等于8,如果大于等于8并且数组长度大于等于64就把链表转化为红黑树。
5.插入成功后,判断已经存在的键值对数量size是否大于扩容阈值(最扩容阈值 = 数组长度 * 加载因子),如果超过,就进行扩容。
说一下HashMap的扩容机制
1.第一次添加数据时,因为还没有初始化过,所以先初始化数组长度为16,扩容阈值为12。
2.每次已经存在的键值对数量size大于等于扩容阈值,要扩容的时候,就把数组长度和扩容阈值都变为原来的2倍。
3.扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新数组中
- 3.1 如果是没有hash冲突的节点,比如把这个节点叫做e,就直接使用e.hash & (newCap - 1)
计算在新数组中的索引位置。
- 3.2 如果是红黑树,就调用红黑树的添加方法。
- 3.3 如果是链表,就开始遍历链表的每个节点,把当前遍历的节点叫做e,如果(e.hash & oldcap) == 0
,那么该节点就还是停留在原来的位置,否则就移动到原来的位置 + 原来的数组大小
这个位置
讲讲HashMap的寻址算法?
HashMap源码里通过一个 hash() 函数对 key 进行哈希,
先得到 key 的 hashCode,然后和右移16位的 hashCode 值做异或操作,他是一个扰动算法,目的是为了让哈希分布更加均匀。
最后通过 hash & (数组长度 - 1)
得到数组下标索引。
为什么HashMap的数组长度一定是2的幂?
这样计算数组下标索引时效率更高,假设数组长度为n,那么久可以用 & (n - 1)
替换 % n
的取模操作。
聊一下ConcurrentMap
jdk1.8版本的ConcurrentMap和1.7版本的ConcurrentMap实现方式不一样。
jdk1.7底层采用分段的数组 + 链表实现。
jdk1.8底层采用的数据结构和HashMap一样,都是数组 + 链表/红黑树。
JDK1.8对底层Entry数组table,size容量都加了votatile的修饰,保证了不同线程对数组table和size的可见性,
JDK1.8采用CAS+synchronized来保证并发状态下的安全性。当数组桶里还没有元素时,就通过CAS的方式新增键值对,因为CAS是原子操作,所以不会造成线程安全问题。如果数组桶里有键值对,就用synchronized锁,锁的对象是链表或者红黑树的首节点,保证了一个数组索引位置同一时间只有一个线程进行操作,实现了线程安全。