掌握ConcurrentHashMap的关键:应对八道问题,展示你的并发数据结构知识

1.ConcurrentHashMap与HashMap有什么区别?

HashMap和ConcurrentHashMap的区别在于线程安全性和底层数据结构。HashMap是线程不安全的,而ConcurrentHashMap是线程安全的。底层数据结构上,JDK1.7中的ConcurrentHashMap使用了分段数组+链表的方式实现,而JDK1.8中的ConcurrentHashMap和HashMap都采用了数组+链表或数组+红黑树的方式实现。

2.说一下ConcurrentHashMap的工作原理,put()和get()的工作流程是怎样的?

存储对象时,将key和vaule传给put()方法:

  • 如果没有初始化,就调用initTable()方法对数组进行初始化;
  • 如果没有hash冲突则直接通过CAS进行无锁插入;
  • 如果需要扩容,就先进行扩容,扩容为原来的两倍;
  • 如果存在hash冲突,就通过加锁的方式进行插入,从而保证线程安全。(如果是链表就按照尾插法插入,如果是红黑树就按照红黑树的数据结构进行插入);
  • 如果达到链表转红黑树条件,就将链表转为红黑树;
  • 如果插入成功就调用addCount()方法进行计数并且检查是否需要扩容;

注意:在并发情况下ConcurrentHashMap会调用多个工作线程一起帮助扩容,这样效率会更高。

下面以一个很详细的流程图方式展现一下ConcurrentHashMap的put()过程(由于流程图比较庞大复杂,所以没有将计数和扩容阶段的流程画出,有兴趣的小伙伴可以去看一下addCount()和transfer()这两个方法的源码):

3a814f293b43d896c06d97d70b157e90.jpeg

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

  • 计算hash值,定位table索引位置,如果头节点符合条件则直接返回key对应的value;
  • 如果遇到正在扩容,则调用标记正在扩容的节点,查找该节点,匹配就返回;
  • 以上条件都不符合,就继续向下遍历;

注意:其实get()的流程跟HashMap基本是一样的。put()的流程只是比HashMap多了一些保证线程安全的操作而已

3.ConcurrentHashMap和HashTable的效率哪个更高?为什么?

ConcurrentHashMap的效率要高于HashTable,因为HashTable是使用一把锁锁住整个链表结构从而实现线程安全。而ConcurrentHashMap的锁粒度更低,在JDK1.7中采用分段锁实现线程安全,在JDK1.8中采用CAS(无锁算法)+Synchronized实现线程安全。

追问:那你具体说一下HashTable和ConcurrentHashMap的锁机制(重点)

HashTable中的锁机制:

HashTab是使用Synchronized来实现线程安全的,是使用一把锁锁住整个链表结构,效率非常低。当有一个线程访问同步方法的时候,其他线程是访问不了的,其他线程可能会被阻塞或者进入轮询状态。如果有一个线程正在执行put()操作的时候,其他线程是不可以进行put()操作的,也不可以进行get()操作,并发线程越多,竞争越激烈,效率越低下。

f15678066f169ca11d8fd03e37cc72e1.jpeg

ConcurrentHashMap在JDK1.7中的分段锁机制:

对整个数组进行分段(每段都是由若干个hashEntry对象组成的链表),每个分段都有一个Segment分段锁(继承ReentrantLock分段锁),每个Segment分段锁只会锁住它锁守护的那一段数据,多线程访问不同数据段的数据,就不会存在竞争,从而提高了并发的访问率。


3fa4d9495ad57d8fb8ccfac0d368ed4b.jpeg

ConcurrentHashMap在JDK1.8中的锁机制:

ConcurrentHashMap在JDK1.8中采用Node+CAS+Synchronized实现线程安全,取消了segment分段锁,直接使用Table数组存储键值对(与1.8中的HashMap一样),主要是使用Synchronized+CAS的方法来进行并发控制。在put()的时候如果CAS失败就说明存在竞争,会进行自旋,具体流程上面已有说明,这里就不在赘述。


90b560cc8d148e517519bc3effaf09a8.jpeg

4.ConcurrentHashMap在JDK1.8中为什么要使用内置锁Synchronized来替换ReentractLock重入锁?

  • 锁粒度降低了;
  • 官方对synchronized进行了优化和升级,使得synchronized不那么“重”了;
  • 在大数据量的操作下,对基于API的ReentractLock进行操作会有更大的内存开销;

5.ConcurrentHashMap的get()方法需要加锁吗?

不需要,get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

6.ConcurrentHashMap中的key和value可以为null吗?为什么?

不可以,因为源码中是这样判断的,进行put()操作的时候如果key为null或者value为null,会抛出NullPointerException空指针异常。

追问:那么源码为什么要这么设计呢?

如果ConcurrentHashMap中存在一个key对应的value是null,那么当调用map.get(key)的时候,必然会返回null,那么这个null就有两个意思:

  • 这个key从来没有在map中映射过,也就是不存在这个key;
  • 这个key是真实存在的,只是在设置key的value值的时候,设置为null了;

这个二义性在非线程安全的HashMap中可以通过map.containsKey(key)方法来判断,如果返回true,说明key存在只是对应的value值为空。如果返回false,说明这个key没有在map中映射过。这样是为什么HashMap可以允许键值为null的原因,但是ConcurrentHashMap只用这个判断是判断不了二义性的。

追问:说说为什么ConcurrentHashMap判断不了呢?

此时如果有A、B两个线程,A线程调用ConcurrentHashMap.get(key)方法返回null,但是我们不知道这个null是因为key没有在map中映射还是本身存的value值就是null,此时我们假设有一个key没有在map中映射过,也就是map中不存在这个key,此时我们调用ConcurrentHashMap.containsKey(key)方法去做一个判断,我们期望的返回结果是false。但是恰好在A线程get(key)之后,调用constainsKey(key)方法之前B线程执行了ConcurrentHashMap.put(key,null),那么当A线程执行完containsKey(key)方法之后我们得到的结果是true,与我们预期的结果就不相符了。

至于ConcurrentHashMap中的key为什么也不能为null的问题,ConcurrentHashMap的作者Doug Lea认为map中允许键值为null是一种不合理的设计,HashMap虽然可以判断二义性,但是Doug Lea仍然觉得这样设计是不合理的。

7.ConcurrentHashMap的并发度是什么?

ConcurrentHashMap的并发度是指程序在运行时能够同时更新ConcurrentHashMap且不产生锁竞争的最大线程数。默认情况下,该并发度值为16,并且可以通过构造函数进行设置。如果手动设置了并发度,ConcurrentHashMap会将实际并发度设定为大于等于该设置值的最小2的幂指数。例如,如果设置值为17,则实际并发度将为32。

8.你认为自己有什么缺点?(HR提问)

回答此问题,注意以下几点即可:

- 不要声称自己没有缺点,这会显得不真实。
- 不要提及会对工作产生负面影响的缺点,因为这相当于直接告诉别人你的致命弱点。
- 可以谈论一些表面上的缺点,但从工作的角度看可以被视为优点的缺点。例如,可以说自己对代码和产品设计追求完美,对自己和工作要求较高。然而,根据具体情况需要做出相应的调整。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值