Java集合详解10:ConcurrentHashmap面试题汇总,最近找工作老是被问!

ConcurrentHashMap篇

HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
  2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

ConcurrentHashMap 和 Hashtable 的区别?

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

两者的对比图

HashTable:

img

JDK1.7的ConcurrentHashMap:

img

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

img

答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。

ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

JDK1.7

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

img

  1. 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
  2. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

JDK1.8

JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

结构如下:

img

附加源码,有需要的可以看看

插入元素过程(建议去看看源码):

如果相应位置的Node还没有初始化,则调用CAS插入相应的数据;

 
  1. else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

  2. if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))

  3. break; // no lock when adding to empty bin

  4. }

如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点;

 
  1. if (fh >= 0) {

  2. binCount = 1;

  3. for (Node<K,V> e = f;; ++binCount) {

  4. K ek;

  5. if (e.hash == hash &&

  6. ((ek = e.key) == key ||

  7. (ek != null && key.equals(ek)))) {

  8. oldVal = e.val;

  9. if (!onlyIfAbsent)

  10. e.val = value;

  11. break;

  12. }

  13. Node<K,V> pred = e;

  14. if ((e = e.next) == null) {

  15. pred.next = new Node<K,V>(hash, key, value, null);

  16. break;

  17. }

  18. }

  19. }

  1. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
  2. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;

15.Java中的另一个线程安全的与HashMap极其类似的类是什么?同样是线程安全,它与HashTable在线程同步上有什么不同?

ConcurrentHashMap类(是Java并发包java.util.concurrent中提供的一个线程安全且高效的HashMap实现)。
HashTable是使用synchronize关键字加锁的原理(就是对对象加锁);
而针对ConcurrentHashMap,在JDK1.7中采用分段锁的方式;JDK1.8中直接采用了CAS(无锁算法)+synchronized

16.HashMap&ConcurrentHashMap的区别?

除了加锁,原理上无太大区别。另外,HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

17.为什么ConcurrentHashMap比HashTable效率要高?

HashTable使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;
ConcurrentHashMap

  • JDK1.7中使用分段锁(ReentrantLock+Segment+HashEntry),相当于把一个HashMap分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于Segment,包含多个HashEntry

  • JDK1.8中使用CAS+synchronized+Node+红黑树。锁粒度:Node(首结点)(实现Map.Entry<K,V>)。锁粒度降低了。

18.针对ConcurrentHashMap锁机制具体分析(JDK1.7VSJDK1.8)?

JDK1.7中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类SegmentHashEntry

  • ①、Segment继承ReentrantLock(重入锁)用来充当锁的角色,每个Segment对象守护每个散列映射表的若干个桶;

  • ②、HashEntry用来封装映射表的键-值对;

  • ③、每个桶是由若干个HashEntry对象链接起来的链表;

JDK1.8中,采用Node+CAS+Synchronized来保证并发安全。取消类Segment,直接用table数组存储键值对;当HashEntry对象组成的链表长度超过TREEIFY_THRESHOLD时,链表转换为红黑树,提升性能。底层变更为数组+链表+红黑树。

19.ConcurrentHashMap在JDK1.8中,为什么要使用内置锁synchronized来代替重入锁ReentrantLock?

  • ①、粒度降低了;

  • ②、JVM开发团队没有放弃synchronized,而且基于JVMsynchronized优化空间更大,更加自然。

  • ③、在大量的数据操作下,对于JVM的内存压力,基于APIReentrantLock会开销更多的内存。

20.ConcurrentHashMap简单介绍?

  • ①、重要的常量:

    • private transient volatile intsizeCtl;

    • 当为负数时,-1表示正在初始化,-N表示N-1个线程正在进行扩容;

    • 当为0时,表示table还没有初始化;

    • 当为其他正数时,表示初始化或者下一次进行扩容的大小。

  • ②、数据结构:

    • Node是存储结构的基本单元,继承HashMap中的Entry,用于存储数据;

    • TreeNode继承Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据;

    • TreeBin是封装TreeNode的容器,提供转换红黑树的一些条件和锁的控制。

  • ③、存储对象时(put()方法):

    • 1.如果没有初始化,就调用initTable()方法来进行初始化;

    • 2.如果没有hash冲突就直接CAS无锁插入;

    • 3.如果需要扩容,就先进行扩容;

    • 4.如果存在hash冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;

    • 5.如果该链表的数量大于阀值8,就要先转换成红黑树的结构,break再一次进入循环

    • 6.如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。

  • ④、扩容方法transfer(): 默认容量为16,扩容时,容量变为原来的两倍。
    helpTransfer(): 调用多个工作线程一起帮助进行扩容,这样的效率就会更高。

  • ⑤、获取对象时(get()方法):

    • 1.计算hash值,定位到该table索引位置,如果是首结点符合就返回;

    • 2.如果遇到扩容时,会调用标记正在扩容结点ForwardingNode.find()方法,查找该结点,匹配就返回;

    • 3.以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回null

21.ConcurrentHashMap的并发度是什么?

程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数。默认为16,且可以在构造函数中设置。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)

微信公众号【程序员书单】

一个为程序员推荐好书的公众号。每周为你pick精品书单,优质学习资源和工具软件。关注前沿技术与行业资讯,更关注你的自我提升。人生苦短,要把时间浪费在美好的事物上~

回复“book”即可领取java后端学习必备20+本电子书。更多电子书下载,请移步至程序员书单官网:coderbooklist.com

微信公众号【Java技术江湖】

一位阿里 Java 工程师的技术小站,专注于 Java 相关技术:SSM、SpringBoot、MySQL、分布式、中间件、集群、Linux、网络、多线程,偶尔讲点Docker、ELK,同时也分享技术干货和学习经验,致力于Java全栈开发!

关注公众号后回复“PDF”即可领取200+页的《Java工程师面试指南》强烈推荐,几乎涵盖所有Java工程师必知必会的知识点。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值