concurrentHashMap面试题

x33g5p2x  于2022-02-07 转载在 Java  
字(3.9k)|赞(0)|评价(0)|浏览(513)

转载于https://blog.csdn.net/yunzhaji3762/article/details/113623168

ConcurrentHashMap 的实现原理是什么?★★★★★

ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的实现方式是不同的。

先说看下JDK1.7

数据结构上:
JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成

锁的实现上:
如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
其中:
Segment继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment数组默认大小是16,因此concurrentHashMap的并发度为 16。

然后Segment类中的HashEntry数组,HashEntry类中的value和next的属性都使用volatile修饰以此来保证多线程环境下数据获取的可见性。

再来说下JDK1.8

数据结构上:
JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构

锁的实现上:
在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?★★★★★

  • JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会有一个【锁升级】的过程
  • 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?★★★★★

ConcurrentHashMap 的效率要高于 Hashtable,因为 Hashtable 给整个哈希表加了一把大锁实现线程安全
ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全

具体说一下Hashtable的锁机制★★★★★

Hashtable 是使用 synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!

JDK1.7 和 JDK1.8的ConcurrentHashMap的区别★★★★

数据结构不同:

  • JDK7先是一个Segment数组,每个Segment下又是 数组 + 链表的形式
  • JDK8是 Node数组 + 链表 + 红黑树的形式

线程安全机制不同:

  • JDK7利用的是分段锁,本质上是ReentrantLock
  • JDK8是CAS + synchronized

锁的粒度不同:

  • JDK7是对需要进行数据操作的Segment加锁(粒度大)
  • JDK8是对每个数组元素进行加锁(粒度小)

ConcurrentHashMap 的 put 方法执行逻辑是什么?★★★★

JDK1.7中:

  1. 先定位到相应的 Segment ,然后再进行 put 操作。
  2. 首先会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
  3. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

JDk1.8中:

大致可以分为以下步骤:

  1. 根据 key 计算出 hash 值;
  2. 判断是否需要进行初始化
  3. 定位到 Node,拿到首节点 f,判断首节点 f
    3.1. 如果为 null,说明 没有出现下标冲突 ,则通过 CAS 的方式尝试添加
    3.2. 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容
    3.3. 如果都不满足 ,则 说明出现了下标冲突,锁住链表头或者树根节点;synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;
  4. 当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树

ConcurrentHashMap 的 get 方法执行逻辑是什么?★★★★

JDK1.7中:

  1. 根据 key 计算出 hash 值定位到具体的 Segment
  2. 再根据 hash 值获取定位 HashEntry 对象
  3. 并对 HashEntry 对象进行链表遍历,找到对应元素

由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以每次获取时都是最新值。


JDK1.8中:

  1. 根据 key 计算出 hash 值,判断数组是否为空
  2. 如果是首节点,就直接返回
  3. 如果是红黑树结构,就从红黑树里面查询
  4. 如果是链表结构,循环遍历判断

HashMap与ConcurrentHashMap区别★★★

  • ConcurrentHashMap是线程安全的,HashMap不是线程安全的
  • HashMap的键值堆允许为null,但是ConcurrentHashMap都不允许

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 ,就有了二义性。(因为在并发环境下,map可能在调用之间发生变化)
  • 而用于单线程状态的 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 了,这就与我们的假设的真实情况不符合了,这就有了二义性。

【作者Doug Leade的回答】

多线程下安全的操作 map还有其他方法吗?★★★

还可以使用Collections.synchronizedMap方法,对方法进行加同步锁。
如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,线程安全,本质也是对 HashMap 进行全表锁。在竞争激烈的多线程环境下性能依然也非常差,不推荐使用!

ConcurrentHashMap 的并发度是什么?★★

并发度可以理解为程序运行时能够同时更新 ConccurentHashMap且不产生锁竞争的最大线程数

在JDK1.7中,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度,默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。

在JDK1.8中,已经摒弃了Segment的概念,选择了Node数组+链表+红黑树结构,并发度大小依赖于数组的大小

ConcurrentHashMap初始大小、加载因子★★

ConcurrentHashMap和HashMap默认初始大小都是16、负载因子都是0.75
负载因子(load factor)是权衡哈希表密集程度的一个参数。
若负载因子过大,则说明哈希表能装载更多元素,出现的hash冲突的概率就越大;
反之,装载越少,出现hash冲突的概率就越小。
同时若负载因子设置过小,很显然内存使用率就不高。
负载因子的取值应考虑到内存使用率和hash冲突概率的平衡
转载于https://blog.csdn.net/yunzhaji3762/article/details/113623168

相关文章