本篇内容:3000+字面试
预计阅读时间:7分钟算法
本文汇总了常考的 ConcurrentHashMap 面试题,面试 ConcurrentHashMap,看这一篇就够了!为帮助你们高效复习,专门用”★ “表示面试中出现的频率,”★ “越多,表明越高频!数组
ConcurrentHashMap 的实现原理是什么? ★★★★★安全
ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的实现方式是不一样的。数据结构
先来看下JDK1.7多线程
JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分红小数组(Segment ),每一个小数组有 n 个 HashEntry 组成。并发
以下图所示,首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其余段的数据也能被其余线程访问,实现了真正的并发访问。ide
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成以下:函数
Segment 继承了 ReentrantLock,因此 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。性能
存放元素的 HashEntry,也是一个静态内部类,主要的组成以下:
其中,用 volatile 修饰了 HashEntry 的数据 value 和 下一个节点 next,保证了多线程环境下数据获取时的可见性!
再来看下JDK1.8
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用 CAS + synchronized实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只须要锁住这个链表头节点(红黑树的根节点),就不会影响其余的哈希桶数组元素的读写,大大提升了并发度。
JDK1.8 中为何使用内置锁 synchronized替换 可重入锁 ReentrantLock?★★★★★
ConcurrentHashMap 的 put 方法执行逻辑是什么?★★★★
先来看JDK1.7
先定位到相应的 Segment ,而后再进行 put 操做。
源代码以下:
首先会尝试获取锁,若是获取失败确定就有其余线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
再来看JDK1.8
大体能够分为如下步骤:
根据 key 计算出 hash 值;
判断是否须要进行初始化;
定位到 Node,拿到首节点 f,判断首节点 f:
当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树。
源代码以下:
ConcurrentHashMap 的 get 方法执行逻辑是什么?★★★★
一样,先来看JDK1.7
首先,根据 key 计算出 hash 值定位到具体的 Segment ,再根据 hash 值获取定位 HashEntry 对象,并对 HashEntry 对象进行链表遍历,找到对应元素。
因为 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 能够保证内存可见性,因此每次获取时都是最新值。
源代码以下:
再来看JDK1.8
大体能够分为如下步骤:
根据 key 计算出 hash 值,判断数组是否为空;
若是是首节点,就直接返回;
若是是红黑树结构,就从红黑树里面查询;
若是是链表结构,循环遍历判断。
源代码以下:
ConcurrentHashMap 的 get 方法是否要加锁,为何?★★★
get 方法不须要加锁。由于 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。
这也是它比其余并发集合好比 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 效率高的缘由之一。
get 方法不须要加锁与 volatile 修饰的哈希桶数组有关吗?★★★
没有关系。哈希桶数组table用 volatile 修饰主要是保证在数组扩容的时候保证可见性。
ConcurrentHashMap 不支持 key 或者 value 为 null 的缘由?★★★
咱们先来讲value 为何不能为 null。由于 ConcurrentHashMap 是用于多线程的 ,若是ConcurrentHashMap.get(key)获得了 null ,这就没法判断,是映射的value是 null ,仍是没有找到对应的key而为 null ,就有了二义性。
而用于单线程状态的 HashMap 却能够用containsKey(key) 去判断究竟是否包含了这个 null 。
咱们用反证法来推理:
假设 ConcurrentHashMap 容许存放值为 null 的 value,这时有A、B两个线程,线程A调用ConcurrentHashMap.get(key)方法,返回为 null ,咱们不知道这个 null 是没有映射的 null ,仍是存的值就是 null 。
假设此时,返回为 null 的真实状况是没有找到对应的 key。那么,咱们能够用 ConcurrentHashMap.containsKey(key)来验证咱们的假设是否成立,咱们指望的结果是返回 false 。
可是在咱们调用 ConcurrentHashMap.get(key)方法以后,containsKey方法以前,线程B执行了ConcurrentHashMap.put(key, null)的操做。那么咱们调用containsKey方法返回的就是 true 了,这就与咱们的假设的真实状况不符合了,这就有了二义性。
至于 ConcurrentHashMap 中的 key 为何也不能为 null 的问题,源码就是这样写的,哈哈。若是面试官不满意,就回答由于做者Doug不喜欢 null ,因此在设计之初就不容许了 null 的 key 存在。想要深刻了解的小伙伴,能够看这篇文章这道面试题我真不知道面试官想要的回答是什么
ConcurrentHashMap 的并发度是什么?★★
并发度能够理解为程序运行时可以同时更新 ConccurentHashMap且不产生锁竞争的最大线程数。在JDK1.7中,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度,默认是16,这个值能够在构造函数中设置。
若是本身设置了并发度,ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数做为实际并发度,也就是好比你设置的值是17,那么实际并发度是32。
若是并发度设置的太小,会带来严重的锁竞争问题;若是并发度设置的过大,本来位于同一个Segment内的访问会扩散到不一样的Segment中,CPU cache命中率会降低,从而引发程序性能降低。
在JDK1.8中,已经摒弃了Segment的概念,选择了Node数组+链表+红黑树结构,并发度大小依赖于数组的大小。
ConcurrentHashMap 迭代器是强一致性仍是弱一致性?★★
与 HashMap 迭代器是强一致性不一样,ConcurrentHashMap 迭代器是弱一致性。
ConcurrentHashMap 的迭代器建立后,就会按照哈希表结构遍历每一个元素,但在遍历过程当中,内部元素可能会发生变化,若是变化发生在已遍历过的部分,迭代器就不会反映出来,而若是变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。
这样迭代器线程可使用原来老的数据,而写线程也能够并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提高的关键。想要深刻了解的小伙伴,能够看这篇文章:http://ifeve.com/ConcurrentHashMap-weakly-consistent/
JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?★★★★★
ConcurrentHashMap 和 Hashtable 的效率哪一个更高?为何?★★★★★
ConcurrentHashMap 的效率要高于 Hashtable,由于 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全。
具体说一下Hashtable的锁机制 ★★★★★
Hashtable 是使用 synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操做该对象,那其余线程只能阻塞等待须要的锁被释放,在竞争激烈的多线程场景中性能就会很是差!
多线程下安全的操做 map还有其余方法吗?★★★
还可使用Collections.synchronizedMap方法,对方法进行加同步锁。
若是传入的是 HashMap 对象,其实也是对 HashMap 作的方法作了一层包装,里面使用对象锁来保证多线程场景下,线程安全,本质也是对 HashMap 进行全表锁。在竞争激烈的多线程环境下性能依然也很是差,不推荐使用!
本篇的 ConcurrentHashMap 就到这里了,以为不错的话,不要忘记点个赞~
小伙伴们想看什么类型的文章,欢迎留言或私信~