1. HashMap常见面试题
1.1 HashMap在jdk7和8两个版本中有什么区别?
- 数据结构不同:
1.7中的HashMap是数组+链表的结构 1.8中的HashMap是数组+链表+红黑树的结构 - 链表插入方式不同:
1.7使用的是头插法,头插法在进行扩容时存在线程安全问题导致链表死循环1.8使用的是尾插法
1.2 HashMap的工作原理是怎样?
- HashMap主要用于存放键值对,由数组、链表、红黑树构成,通过对键的hash值计算与数组最大索引进行与运算,从而获得在数组中存储的位置。如果发生哈希冲突(key的哈希值一样)则会形成链表,新的元素将会放在最后。在链表长度大于8的情况下,如果当前数组容量大于64时会根据当前链表形成红黑树,反之如果当前数组容量小于64时会继续对数组进行扩容(不生成红黑树)。如果当前存在的红黑树中节点数小于6时会转换回链表(能满足不生成红黑树的条件就不生成红黑树,因为红黑树的删除,左旋,右旋特别耗时)
1.3 HashMap是如何确定键值对的位置?如何解决Hash冲突?
- 首先如果key为null 则都会被放置在数组的第0位
- 如果不为空则通过key的HashCode方法获得hash值并与自身右移16位进行异或运算(是为了让高位低位同时参与运算,让哈希的散列算法更加均匀分配)后再与最大索引进行与运算得到数组中的索引获得元素或再到链表或红黑树中查询。
- 如果发生Hash冲突就会形成链表,我们只能尽量避免Hash冲突无论是在hashCode后获得的值在与自己进行一次异或运算还是每次扩容都只扩容2的次幂都是为了尽量减少hash碰撞,提高查询效率。
1.4 HashMap存值过程中什么时候进行数组扩容?
- 在存值后对数组容量大小进行一个判断,当前数组存储数量>加载因子*当前容量时就进行扩容
- 当链表大于8时数组容量小于64时进行扩容。
1.5 HashMap扩容为什么每次都是2的次幂?
- 为了减少hash冲突,只有当数组大小为2的次幂时,数组最大索引的二进制表示每个位置都是1,从而使与运算的分布更均匀。
1.6 HashMap底层为什么要使用异或运算符?
- 为了减少Hash冲突,我们最后要使用Hash值与数组最大索引进行与运算获取索引,比如我们的数组大小是16数组最大索引是15二进制表示为1111,运算结果会取决于哈希值的低4位,这样看起来只有极少情况下能使哈希值的高16位参与运算,所以我们用哈希值与他自己的高16位进行异或运算,通过让高16位参与运算使下标分布更均匀,不使用&或|是因为他们的结果都偏向于0或者1不符合我们的目的。
1.7 HashMap的线程安全问题发生在哪个阶段?
- 很多位置都会出现线程安全问题,比如添加键值对的时候会出现覆盖数据问题,如下所示
package com.example.springboot04.test;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
public class TestHashMap {
/**
* 测试hashMap的线程安全问题
*/
public static void main(String[] args) throws InterruptedException {
//创建公共资源HashMap
Map<Object,Object> map=new HashMap<>();
//创建两个Thread匿名内部类
new Thread(){
@Override
public void run() {
for (int i = 0; i < 2500; i++) {
map.put(i,i);
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i = 2500; i < 5000; i++) {
map.put(i,i);
}
}
}.start();
//让main线程睡个5秒
Thread.currentThread().sleep(5000);
//因为每个key的hash值都不一样,按理说size=5000
System.out.println(map.size());
}
}
所以当两个线程同时对map容器进行put操作时,后执行的线程会对前一个线程的put操作进行一个覆盖
1.8 HashMap和ConcurrentHashMap有什么区别?
- HashMap线程不安全但效率高,ConcurrentHashMap线程安全但是效率比HashMap要低,ConcurrentHashMap会有更多的变量来支持线程安全所需要的乐观锁,在put方法中也加入了synchronized代码块锁住了当前的节点头使其只能一个线程访问,在扩容时其他线程添加元素会被搁置并且协助进行扩容,并声明自己正在处理哪个位置。
1.9 ConcurrentHashMap是如何实现线程安全的?
- 通过乐观锁与synchronized来实现线程安全,在初始化数组与空节点添加元素的时候使用乐观锁来实现线程安全,在给非空节点添加元素的时候会使用synchronized代码块锁住当前节点头从而实现线程安全,而在扩容的时候会将转移后的节点单独保存起来以便于在扩容中的put与get操作,如果put需要操作的元素还没有转移完成则会被搁置等待转移完成,被搁置的线程也会被分配到转移数据的任务从而协助扩容。
2.红黑树结构(左旋,右旋)
2.2 红黑树性质(重点)
- 每个节点不是黑色就是红色
- 不可能有连在一起的子父红色节点
- 根节点都是黑色 root
- 每个红色节点的两个子节点都是黑色
2.3 变换规则
旋转和颜色变换规则:所有插入的点默认为红色
2.3.1 变颜色的情况:当前结点的父亲是红色,且它的祖父结点的另一个子结点也是红色(叔叔结点)
- 把父结点设置为黑色
- 把叔叔结点设为黑色
- 把祖父也就是父亲的父亲设为红色(爷爷)
- 把指针定义到祖父节点(爷爷)设置为当前要操作分析的点变换规则
2.3.2 左旋:当前父节点是红色,叔叔是黑色的时候,且当前的节点的右子树。以父节点作为左旋![在这里插入图片描述](https://img-blog.csdnimg.cn/20200707093103664.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTYwOTk4MQ==,size_16,color_FFFFFF,t_70#pic_center)
2.3.3 右旋:当前父节点是红色,叔叔是黑色的时候,且当前父节点的左子树。![在这里插入图片描述](https://img-blog.csdnimg.cn/20200707093325415.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTYwOTk4MQ==,size_16,color_FFFFFF,t_70#pic_center)
- 把父结点变为黑色
- 把祖父节点变为红色
- 以祖父节点进行右旋转