HashMap经典21问

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()方法:

  1. 调用hash(K)方法计算K的哈希值,然后结合数组长度,计算得数组下标
  2. 调整数组大小(当容器中的元素个数大于capacity * loadfactor时,容器会进行扩容为2n)
  3. 如果K的hash值在HashMap中不存在,则进行插入。若存在,则进行碰撞。
  4. 存在且equals返回true,则更新键值对
  5. 存在且equals返回false,则插入链表的尾部(尾插法)或者红黑树中(jdk1.7之前采用头插法,1.8之后才用尾插法)当碰撞导致TREEIFY_THRESHOLD = 8时,就要把链表转化为红黑树。

获取对象时,将K传给get()方法:

  1. 调用hash(K),计算哈希值,从而获取数组下标
  2. 顺序遍历链表,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?

  1. 降低了锁粒度
  2. 在大量的数据操作下,基于API的ReentrantLock会开销更多的内存
  3. 在未来,基于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。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值