谈谈关于Java的hashMap的面试题目吧

  1. 对HashMap了解吗?

   hashmap的话,是分为两个版本来说,首先是JDK1.7的版本:hashMap的数据结构采用的是数组+链表,并且它插入数据的方式是采用的头插法。JDK1.8版本的HashMap,就对1.7时的解决hash碰撞采用链表的方式进行了+链表+红黑树。并且只有在数组长度达到64并且链表长度大于8的时候,才会将链表转换成红黑树,并且对于链表的插入有头插法改成了尾插法。

  1. 你知道他的扩容的原理吗?

   它的扩容的话,首先会判断老表是否超过上限(Integeter最大值),超过的话,直接结束。没有超过的话,容量、扩容阈值变为原来的2倍, table指向新的数组,会去遍历处理老数组,判断当前遍历的索引位置是否有节点,没有的话继续遍历,有的话会判断当前节点是否只有一个节点,是的话,计算该节点在新表的索引位置,直接放到新表,会判断老数组节点是否处理完,没有完得到话,继续遍历。如果当前不止一个节点,会判断该节点是什么,如果是红黑树的话,会去遍历处理该索引位置的红黑树节点,去判断e.hash & oldCap ==0(节点的hash),是的话节点就加到llowhead链表的尾部,不是的话,就加到hiHead链表的尾部,然后,lohead的处理就是,放到原索引位置,红黑树链表节点转换处理,hiHead的处理是,放到原索引+OldCap位置、红黑树链表节点处理转换,也会去判断是否处理完毕。如果这个这个节点是链表的话,回去遍历处理该索引位置的链表节点,回去判断e.hash & OldCap ==0,是的话加到llowhead链表尾部,不是就加到HiHead链表的尾部,lohead放到原索引位置,hihead放到原索引+oldCap位置,去判断是否处理完毕,没有完毕的会,重复以上操作。

  1. 说到他的扩容,那他是如何确定他扩容后的位置的呢?

   采用了与运算的方式来计算,将他扩容的容量-1之后,进行按位与计算,得到的结果会是原先的位置或者原位置+老的容量
对同一个索引进行一次新的与运算
(n-1)& hash
        0000 0000 0000 0000 0000 0000 0001 0000 16(老数组容量)
        0000 0000 0000 0000 0000 0000 0000 1111 15(n-1)
hash1(key1) 1111 1111 1111 1111 0000 1111 0000 0101 key1的hash
-------------------------------------------------------------------------------------------------------------------
       0000 0000 0000 0000 0000 0000 0000 0101 计算出索引是5
        0000 0000 0000 0000 0000 0000 0000 1111 15(n-1)
hash1(key2) 1111 1111 1111 1111 0000 1111 0001 0101 key2的hash
-------------------------------------------------------------------------------------------------------------------
       0000 0000 0000 0000 0000 0000 0000 0101 计算出索引是5
扩容后16*2 = 32
(n-1)& hash
       0000 0000 0000 0000 0000 0000 0010 0000 32(老数组容量)
       0000 0000 0000 0000 0000 0000 0001 1111 31(n-1)
hash1(key1) 1111 1111 1111 1111 0000 1111 0000 0101 key1的hash
-------------------------------------------------------------------------------------------------------------------
       0000 0000 0000 0000 0000 0000 0000 0101 计算出索引是5
        0000 0000 0000 0000 0000 0000 0001 1111 31(n-1)
hash2(key2) 1111 1111 1111 1111 0000 1111 0001 0101 key2的hash
-------------------------------------------------------------------------------------------------------------------
       0000 0000 0000 0000 0000 0000 0001 0101 计算出索引是21

  1. 那你谈谈他put的原理吗?

  当我们插入的时,会先判断数组是否为空,如果是为空的话,会对数组进行初始化工作,然后会先对key进行hash计算,确定他要放在的地方,如果放的位置没有节点,就会新增节点,然后放到先前计算的索引的位置,然后会去判断是否超过阈值,超过的话,就会进行扩容,没有就直接插入,如果要放的位置有节点的话,就会判断头结点的key是否与插入的相同,如果相同的话就会直接覆盖掉原来的值,并返回覆盖前的值,如果不相同的话,就会判断头节点是不是一个红黑数的头节点,是的话,就会找到红黑树的根节点进行遍历,去判断是不是有相同的key,有的话也是直接覆盖掉,没有的话,就会新建节点,放到红黑数对应的位置,然后在进行红黑树的插入平衡调整,然后也会去判断是否超过阈值,超过就扩容,没超过就不扩容,如果不是一个红黑树的话,便会去遍历链表,同时统计他的节点个数,会判断是否有与key相同的节点,有点话就覆盖,没有的话就会插入到链表的尾部,然后判断他的节点个数是否超过8,没有的话就会去判断是否超过阈值,超过的话进行扩容,没有便不扩容,如果超过了8,同时数组的长度也是超过64的便会去转变为红黑树,然后也会去进行判断是否阈值,超过扩容,没超过便不扩容。

  1. 那有线程安全的map吗?

  线程安全的Map的最基础的就是hashTable了,但是HashTable得到效率比较慢,因为他底层使用的Synchronized锁住了整个的hash桶,一般现在都是使用的ConcurrentHashMap。他的效率更加的快。

  1. 那你能说说HashTable他与我们使用的hashMap直接有什么区别吗?

  他们两个最大的区别的话就是一个是线程安全的一个不是线程安全的,其次的话就是,HashTable是不允许存放null的,而HashMap的是允许的,并且hashmap存放的null是固定方向hash表索引位为0的地方。

  1. 那ConcurrentHashMap有是怎么保证的呢?

   ConcurrentHashMap他保证线程的安全的是有两个版本的,首先也是JDK1.7版本,它采用了segment+HashEntry的方式进行实现,它将数据分为一段一段的存储,然后给每一段数据配一把锁,当线程占用锁访问其中一个段的数据时,其他的段的数据也能被其他线程也能访问。由于在1.7的Segment臃肿的设计,在1.8的时候,取消掉它,并采用Node+CAS+Synchronized的方式来保证并发的安全实现,而且synchronized只锁定当前链表或者红黑树的首节点,并且只要不产生hash冲突,就不会产生并发,效率翻倍。

  1. 那你知道SizeCtl吗,能讲讲它的一些含义吗?

  Sizectl为0:表示数组没有进行初始化,且数组初始容量为16
  Sizectl为1为正数:如果数组没有初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么记录的就是数组的扩容阈值。
  Sizectl为-1:表示数组正在进行初始化。
  Sizectl为小于0:并且不是-1的时候,表示数组正在扩容,-(1+n)表示此时有n个线程正在共同完成数组扩容操作。

  1. ConcurrentHashMap他是如何进行数组初始化的呢?

   ConcurrentHashMap的初始化的话采用的是CAS+自旋的方式来保证线程安全。
  线程会先判断前面sizeCtl是不是小于0,是的就有其他线程正在进行初始化、
假如一个线程判断了不是小于0的,首先就会调用compareAndSaplnt(this,SizeCtl,sc,-1)进行判断,如果将sizeCtl修改为-1成功,当前线程就会去对数组进行初始化,初始化的时候,会去计算一个扩容阈值(n-(n>>>2) )相当于0.75,修改失败的就会进行自选等待。最后会将计算好的扩容阈值复制给sizeCtl。
----->计算扩容阈值—>很妙,他是n- (n>>>2)也就是相当于0.75

  1. 能讲讲他put或者扩容的原理吗?

  就说一下他put的原理吧,相对于HashTable的put的时候的加锁,currentHashMap put的时候加锁他锁的是对这个添加的桶进行的加锁,不会去影响其他桶位的线程操作,而不是像HashTable—一样他锁的是整个hash表。
在put之前会判断一下key和value是不为空的(null),是的话会抛出一个空指针异常。然后是写了一个死循环,先去判断数组有没有进行初始化,进行hash计算,计算出当前桶位置有没有元素,没有的话就采用CAS进行添加元素,当计算出当前桶位置的元素是MOVED的话,就证明正在扩容,就会去协助扩容,还有就是,当计算出当前桶位置不为空的时候,并且也没有在扩容的时候,就会进行添加元素,然后采用的是一个synchronized(同步代码块),他锁的是这个桶(锁的是synchronized (f){)f是上面计算出当前桶就是数组的下标对应的位置),而不是像hashTable。进行添加的时候就会判断他是不是一个普通的链表节点,是普通的链表节点的时候就直接安按照尾插进行添加,如果是红黑树就会遍历红黑树,判断有无相同的,然后进行添加。在最后的时候,会去判断一个binCount是不是达到树化的标准,是的话就会去树化,这一点和HashMap一样(数组长度大于64,链表长度大于等于8)
  然后扩容的话其实就是,会去首先判断一个cup数是不是大于1的,是的话会给每个线程划分任务,然后就判断当前线程是不是扩容线程,是的话就是按照两倍扩容新数组,记录线程开始迁移的桶位,然后从后往前开始迁移。
会在已经迁移的桶位,用一个fwd节点去占位,表示已经迁移了,这个节点里面的hash值moved—> -1。如果没有迁移的话,他会进行加锁,用一个代码块(synchronized (f) 0)将这个桶锁住,进行数据的迁移(具体迁移和hashMap类似的),线程各自只会负责各自的迁移任务。

  1. 那在问你一个ConcurrentHashMap他的Get方法是否需要加锁呢?

  get方法是不需要加锁的,因为Node的元素val和指针next都是使用volatile来修饰的,在多线程情行下,线程T1修改了节点的val或者新增节点的时候多线程T2是可见的。

  1. Get方法不需要加锁与volatile修饰hash桶有关吗?

  没有关系的,hash桶Table用volatile来修饰主要是保证数组扩容的时候的可见性。

13.那最后在问你一个问题,就是 为什么ConcurrentHashMap它不支持key或者value为null呢?

  value为null,因为concurrentHashMap是多线程,当通过get (key)去获取值得到null,无法判断这个映射是null还是没有找到对应的key而为null,单线程hashmap下可以使用containskey去判断到时是否包含了这个null。还有就是如果允许ConcurrentHashMap允许存放值为null的value的话,这时候有两个线程,T1线程调用get (key)返回null,我们是不知道这个null是没有映射为null还是本来就是null,假如这时就是没有找到对应的key,我们调用ContaintsKey来验证的话期望得到的是false,但是,如果我们在调用get和ContainsKey之间,另一个线程T2执行put (key,null)的操作,那么我们调用ContainsKey返回的就是true,就是与假设不符合。就有了二义性。

至于key为什么不能为null,我猜的话是作者doug lee不喜欢null,就好像是注释中写到在并发情况下检查空键和值很困难,设计之初就不允许了key为null,具体的话也没找到一个官方的解释。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值