文章目录
- HashMap的特点?
- 转为红黑树的条件是什么?1.8后为什么加入红黑树?
- HashMap的结构
- 谈一下hashMap中put是如何实现的?
- HashMap是如何确定键值对的位置?如何解决Hash冲突?
- HashMap底层为什么要使用异或运算符?
- HashMap扩容为什么每次都是2的次幂?
- 怎么扩容的?
- HashMap中的加载因子为什么是0.75,如果调整为1呢?
- 为什么重写equals还要重写hashCode方法?
- Hashmap的结构,1.7和1.8有哪些区别?、
- 为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键
- HashMap的线程安全问题发生在哪个阶段?
- ConcurrentHashMap是如何实现线程 安全的?
HashMap的特点?
- HashMap是以键值对的方式存储数据的
- 允许空键和空值(但空键只有一个,且放在第一位)
- Key无序不可重复的,重复将顶替。
- 底层实现是 链表数组,
JDK 8 后
又加了红黑树。 - 线程不安全
转为红黑树的条件是什么?1.8后为什么加入红黑树?
- 当
链表长度>8
会调用treeifyBin(tab, hash);
判断散列长度是否大于64,大于就树化
,小于就扩容,
链表长度 == 6会进行缩容退回链表,这也是为什么选择8的原因留个7作为缓冲地带避免之间频繁切换。
- 涉及服务攻击,当有人找到hash碰撞值,
不断产生hash碰撞就会导致链表长度很长
,当你需要对这个HashMap的相应位置进行查询的时候,就会去循环遍历这个超级大的链表,性能极其低下。java8使用红黑树来替代超过8个节点数的链表后,查询方式性能得到了很好的提升,
从原来的是O(n)到O(logn),容器中节点分布在hash桶中的频率遵循泊松分布
,桶的长度超过8的概率非常非常小(约为10万分之一),所以作者应该是根据概率统计而选择了8作为阀值。
HashMap的结构
1.8
数组+链表+红黑树
构成,每个数据单元为一个Node结构,Node结构中有key字段、value字段、next字段、hash字段- next字段就是发生Hash冲突的时候,
当前桶位中的Node与冲突Node连接成一个链表所需要的字段
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
谈一下hashMap中put是如何实现的?
1.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)
2.如果散列表为空时,调用resize()初始化散列表
3.如果没有发生碰撞,直接添加元素到散列表中去
4.如果发生了碰撞(hashCode值相同),进行三种判断
- 若key地址相同或者equals后内容相同,则替换旧值
- 如果是红黑树结构,就调用树的插入方法
- 链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
5.如果桶满了大于阀值,则resize进行扩容
HashMap是如何确定键值对的位置?如何解决Hash冲突?
- 如果key为null则都会被放置在数组的第0位。
- 如果不为空则通过
key的HashCode方法获得hash值并与自身右移16位
进行异或运算
后再与
最大索引进行与运算得到数组中的索引获得元素或再到链表或红黑树中查询。(n - 1) & hash
HashMap底层为什么要使用异或运算符?
- 其实是为了减少碰撞,进一步降低hash冲突的几率。int类型的数值是4个字节的,右移16位异或可以同时保留高16位于低16位的特征
HashMap扩容为什么每次都是2的次幂?
- 如果发生Hash冲突就会形成链表,每次扩容都只扩容2的次幂都是为了尽量减少hash冲突,提高查询效率。因为只有当数组大小为2的次幂时,
数组最大索引的二进制表示每个位置都是1
,从而使与运算的分布更均匀。
怎么扩容的?
- 当
Node个数到达扩容阈值就会扩容
,而扩容的算法是容量 x 负载因子
,这就是设置负载因子0.75的原因之一,因为2的次方x0.75都是整数。
扩容分为两步
- 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
- ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
为什么要重新Hash呢,直接复制过去不行吗?
- 是因为长度扩大以后,Hash的规则也随之改变。 比如原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你
位运算出来的值
明显不一样了。
HashMap中的加载因子为什么是0.75,如果调整为1呢?
- 加载因子为0.75是官方给出的数值,在官方给出的注释中也表明了这是一个折中的选择,加载因子越小空间利用率越低,查询效率越高,而约接近1空间利用率越高,效率越低。调整为1则当HashMap中数组每个位置都有键值对时才进行扩容。
为什么重写equals还要重写hashCode方法?
- 主要原因是默认从Object继承来的hashCode是基于对象的ID实现的。
如果你重写了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变,那么很可能某两个对象明明是“相等”,而hashCode却不一样。
这样,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找另一个作为键值去查找他们的时候,则根本找不到。 - ps:重写equals的效果只要是同一个类就相等。
public static void main(String[] args) {
User u1 = new User();
User u2 = new User();
// System.out.println(u1.equals(u2)); //未equals,false
//
System.out.println(u1.hashCode());
System.out.println(u2.hashCode());
Map map = new HashMap();
map.put(u1, "1");
map.put(u2, "2");
System.out.println(map.size());//结果为1,重写的目的就是想我是用户就给我返回固定的值就好了
}
Hashmap的结构,1.7和1.8有哪些区别?、
- JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够
避免出现逆序且链表死循环的问题
。 - 扩容算法不一样
为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键
HashMap的线程安全问题发生在哪个阶段?
- 各种阶段都会触发线程安全问题,因为
table
是成员变量,当多线程访问会产生竞态条件(结果无法预测) - (1)在jdk1.7中,在多线程环境下,扩容时
会造成环形链或数据丢失。
(2)在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
ConcurrentHashMap是如何实现线程 安全的?
- Hashtable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问Hashtable的线
程都必须竞争同一把锁。
- 锁分段技术 假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访 问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap所使用的锁分段技术。
读操作时不需要加锁
,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
可以看成行锁。