HashMap经典21问
HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但是遍历顺序却是不确定的。HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任意时刻如果有多个线程同时写HashMap,可能会导致数据的不一致,如果需要满足线程安全,可以用Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
1、HashMap的数据结构?
哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。当链表长度超过8时,链表转换为红黑树。
2、HashMap的工作原理?
HashMap底层是由hash数组和单向链表实现,数组中的每个元素都是链表,由Node内部类(实现Map.Entity)实现,HashMap通过put & get方法存储和获取数据。
存储数据时,将K/V键值传给put()方法:
- 调用hash(K)方法计算K的哈希值,然后结合数组长度,计算得数组下标
- 调整数组大小(当容器中的元素个数大于capacity * loadfactor时,容器会进行扩容为2n)
- 如果K的hash值在HashMap中不存在,则进行插入。若存在,则进行碰撞。
- 存在且equals返回true,则更新键值对
- 存在且equals返回false,则插入链表的尾部(尾插法)或者红黑树中(jdk1.7之前采用头插法,1.8之后才用尾插法)当碰撞导致TREEIFY_THRESHOLD = 8时,就要把链表转化为红黑树。
获取对象时,将K传给get()方法:
- 调用hash(K),计算哈希值,从而获取数组下标
- 顺序遍历链表,equals方法查找相同Node链表中K值对应的V值
注意:hashCode()是定位的存储位置;equals是定性的,比较两者是否相等。
3、两个对象的hashCode相同会发生什么?
hashCode相同,但不一定相等(equals比较的话)。两个对象所在的数组的下标相同,碰撞就此发生。又因为HashMap使用链表存储对象,这个Node会存储到链表中。
4、hash的实现?为什么这样实现?
JDK1.8中,是通过hashCode()的高16位异或低16位实现的。(JDK8 java.util.HashMap.java 339行)原因:主要从速度、功效和质量来考虑的,减少系统的开销,不会因为高位没有参与下标的计算而引起碰撞。
5、为什么要用异或运算符?
保证hashCode的32位只要有一位发生了改变,整个hash()返回值就会变,尽可能地减少碰撞。
6、HashMap的Table容量如何确定?loadFactor是什么?该容量如何变化?这种变化会带来什么问题?
- table数组的大小是由capacity这个参数(JDK8 java.util.HashMap.java 235行)确定的,默认是16,也可以构造时传入,最大限制是1<<30(243行)
- loadFactor是装载因子,主要目的使用来确认table数组是否需要动态扩展,默认是0.75(248行)
- 扩容时,调用resize()方法,将table数组长度变为原来的两倍(677行)
- 如果数据很大,扩展将会带来性能的损失,在性能要求较高的地方,这种损失可能是致命的
7、HashMap中put()执行过程?
调用hash()获取K对应的哈希值,在计算出其数组下标;计算下标方法,10000(2) -1 & hash() 1111 & xxxx 得到0000—1111,为下标
如果没出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面,如果链表长度超过8(258行:TREEIFY_THRESHOLD),则将链表转换为红黑树。链表长度低于6(265行:UNTREEIFY_THRESHOLD),就把红黑树转为链表。
如果节点key已经存在,则替换value即可
如何集合中的键值对数量大于12(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR ),则调用resize方法进行扩容
8、数组扩容的过程?
创建一个新的数组,其容量为就数组的两倍,并重新计算旧数组中节点的存储位置。有两种可能:原下标或原下标+原数组长度
9、拉链法导致的链表过深问题为什么不采用二叉搜索树而选择红黑树?为什么不一直使用红黑树?
选择红黑树就是为了解决二叉树搜索的缺陷,二叉搜索树在特殊情况下会变成一条线性结构,这跟原先的链表一样,也会很深,搜索会非常慢。
而红黑树在插入数据后需要通过左旋、右旋和变色这些操作来保持平衡,引入红黑树是为了查找数据快解决链表查询深度的问题,红黑树属于平衡二叉树,为了保持平衡需要付出代价,但是还是比遍历线型链表要快。所以当深度大于8时,采用红黑树。如果链表长度不够深的话,强行引入效果反而会更差。
10、你对红黑树的理解?
- 每个节点非红即黑
- 根节点总是黑色
- 红色的子节点必定为黑色
- 叶子节点均为黑色空节点(NIL)
- 从根节点到叶子节点或NIL节点,路径中包含相同个数的黑色节点(黑色高度相同)
11、jdk8中对HashMap做了哪些改变?
- jdk1.8引入了红黑树,深度大于8且数组容量不小于64时将转换为红黑树,否则只会进行扩容,小于等于6将转换为链表
- jdk1.7采用头插法处理碰撞,jdk1.8采用尾插法
- jdk1.8中用Node替代了Entry
12、HashMap、LinkedHashMap和TreeMap有什么区别?
- LinkedHashMap 保存了记录的插入顺序,用迭代器遍历先取到的一定时先插入的
- TreeMap 实现了SortMap接口,能够把保存的记录根据键排序。默认升序。
13、HashMap、LinkedHashMap和TreeMap有什么应用场景?
- HashMap:在Map中插入、删除和定位元素时
- TreeMap:在需要按照自然顺序或者自定义顺序遍历元素时
- LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下
14、HashMap和HashTable有什么区别?
- 线程安全方面:HashMap线程不安全,HashTableTable是线程安全的
- 效率方面:由于线程安全,HashTable效率比不上HashMap
- 用法方面:HashMap最多只允许一条记录为null的键允许多条记录为null的值,而HashTable均不允许null键或null值
- 扩容方面:HashMap默认数组长度为16,扩容时两倍。HashTable默认长度为11,扩容时两倍加一
- hash值方面:HashMap需要异或重新计算hash值,而HashTable直接使用键的hashCode
15、Java中另一个线程安全的与HashMap极其相似的类是什么?同样是线程安全,它与HashTable在同步上有什么区别?
ConcurrentHashMap类是Java并发包java.util.concurrent中提供的一个线程安全却高效的HashMap的实现。
HashTable是使用synchronize加锁的原理(就是对对象加锁)
而ConcurrentHashMap,在jdk1.7中采用分段锁的方式;jdk1.8中直接采用了CAS+synchronized
16、HashMap与ConcurrentHashMap的区别?
除了加锁,原理上并无区别。另外,HashMap允许null键和null值。ConcurrentHashMap不允许null键和null值。
17、为什么ConcurrentHashMap效率要比HashTable高?
HashTable使用一把锁,处理并发问题。多个线程竞争一把锁,容易阻塞。
ConcurrentHashMap
- JDK1.7中使用分段锁(ReentrantLock+Segment+HashEntry),相当于把HashMap分成多段,每一段分配一把锁,从而支持多线程访问。基于Segment,包含多个HashEntry
- JDK1.8中使用CAS+synchronized + Node + 红黑树。锁粒度:Node,粒度比起JDK1.8降低了
18、ConcurrentHashMap锁机制具体分析?
JDK1.7中,采用分段锁机制实现并发的更新操作,底层采用树组+链表的存储结构,包括两个核心静态内部类Segment、HashEntry
- Segment继承ReentrantLock(可重入锁)用来充当锁的角色,每个Segment对象守护每个散列映射表的若干个桶
- HashEntry用来封装映射表的键-值对
- 每个桶是由若干个HashEntry对象链接起来的链表
JDK1.8中,采用Node+CAS+Synchronized来保证并发安全
19、ConcurrentHashMap在JDK1.8中,为什么要使用synchronized代替可重入锁ReentrantLock?
- 降低了锁粒度
- 在大量的数据操作下,基于API的ReentrantLock会开销更多的内存
- 在未来,基于JVM的synchronized优化空间更大,更加自然
20、ConcurrentHashMap的简单介绍?
重要的常量
private transient volatile int sizeCtl;
- 为负数时,-1表示正在初始化,-N表示N-1个线程正在进行扩容。
- 为0时表示table还未初始化。
- 为其它正数时,表示初始化或者下一次进行扩容的大小。
数据结构
- Node是存储结构的基本单元,继承HashMap中的Entry,用于存储数据
- TreeNode继承Node,但是数据结构换成了二叉树,是红黑树的存储结构,用于红黑树中存储数据。
- TreeBin是封装TreeNode的容器,提供转换红黑树的条件和锁的控制
存储对象时put方法
- 如果没有初始化,就调用initTable()方法来进行初始化
- 如果没有hash冲突就直接CAS无锁插入
- 如果需要扩容就先进行扩容
- 如果存在hash冲突,就加锁来保证线程安全。链表就用尾插法,红黑树就用红黑树插入方法
- 如果链表长度大于8,就先转换成红黑树,break在一次进入循环
- 如果添加成功就调用addCounter方法统计size,并判断是否需要扩容
扩容方法
- transfer():默认容量为16,扩容后容量变为两倍
- helpTransfer():调用多个线程一起帮助扩容,效率更高
获取对象时get方法
- 计算hash值,定位到该table索引位置,如果首节点符合,就直接返回首节点
- 如果遇到扩容,会调用标记正在扩容节点Forwarding.find()方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则到最后返回null
21、ConcurrentHashMap的并发度是多少?
程序运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数。默认为16,且可以在构造函数中设置。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度。例如设置为17,实际为32。