面试被问到 ConcurrentHashMap答不出 ,看这一篇就够了!

本文汇总了常考的 ConcurrentHashMap 面试题,面试 ConcurrentHashMap,看这一篇就够了!为帮助大家高效复习,专门用”★ “表示面试中出现的频率,”★ “越多,代表越高频!

实现原理

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

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

先来看下JDK1.7

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

如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。

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?★★★★★

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

存取

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

先来看JDK1.7

先定位到相应的 Segment ,然后再进行 put 操作。

源代码如下:

首先会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。

  1. 尝试自旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

再来看JDK1.8

大致可以分为以下步骤:

  1. 根据 key 计算出 hash 值;

  2. 判断是否需要进行初始化;

  3. 定位到 Node,拿到首节点 f,判断首节点 f:

    • 如果为 null ,则通过 CAS 的方式尝试添加;
    • 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容;
    • 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;
  4. 当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树。

源代码如下:

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

同样,先来看JDK1.7

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

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

源代码如下:

再来看JDK1.8

大致可以分为以下步骤:

  1. 根据 key 计算出 hash 值,判断数组是否为空;

  2. 如果是首节点,就直接返回;

  3. 如果是红黑树结构,就从红黑树里面查询;

  4. 如果是链表结构,循环遍历判断。

源代码如下:

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 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。

这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?★★★★★

  • 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  • 保证线程安全机制:JDK1.7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS+synchronized保证线程安全。
  • 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8 调整为对每个数组元素加锁(Node)。
  • 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量大于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  • 查询时间复杂度:从 JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。

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

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

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

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

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

还可以使用Collections.synchronizedMap方法,对方法进行加同步锁。

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

最后

本篇的 ConcurrentHashMap 就到这里了,觉得不错的话,不要忘记点个赞~

为帮助开发者们提升面试技能、有机会入职BATJ等大厂公司,特别制作了这个专辑——这一次整体放出。

大致内容包括了: Java 集合、JVM、多线程、并发编程、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat等大厂面试题等、等技术栈! 

需要获取以下这些面试题答案以及学习资料得话麻烦一键三连之后微信扫描下图作者助手的微信:( wjn168178 )添加即可免费获取到哦

å¨è¿éæå¥å¾çæè¿°

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。

  3. 同时可以期待后续文章ing🚀

  4. .关注后回复【666】扫码即可获取学习资料包

作者:程序员库森
链接:https://www.imooc.com/article/314726

<think>好的,我现在需要回用户关于JavaConcurrentHashMap的用法和最佳实践的问题。首先,我应该回顾一下ConcurrentHashMap的基本概念和它在Java中的应用场景。ConcurrentHashMapJava并发包中的一个线程安全的哈希表实现,适合多线程环境下使用,以提高并发性能。 接下来,我需要确定用户可能关心的关键点。通常,使用ConcurrentHashMap时,需要注意其线程安全机制、如何正确地进行初始化、常用方法的使用,以及与HashMap和Hashtable的区别。此外,最佳实践可能涉及如何选择并发级别、处理null值的问题,以及迭代器的弱一致性特性。 然后,我应该检查用户提供的引用内容。引用[2]提到在多线程环境中使用WeakHashMap时,同步方法intern()可能成为竞争点,并建议在真实场景中使用ConcurrentHashMap。这说明ConcurrentHashMap在解决多线程同步问题上的优势。因此,在回中需要强调ConcurrentHashMap的线程安全实现,比如分段锁或CAS操作,以及如何减少锁竞争。 接下来,我需要组织内容结构。首先介绍ConcurrentHashMap的基本概念,然后详细说明其使用方法,包括初始化、常用方法、线程安全操作等。之后,列出最佳实践,比如避免不必要的锁竞争、合理设置并发级别、处理null值的注意事项等。最后,生成相关问题,帮助用户进一步学习。 在编写过程中,要确保符合用户提供的系统级指令,例如行内数学表达式使用$...$格式,但在这个问题中可能不太涉及数学公式,因此需要注意是否有相关部分。同时,引用标识需要自然地添加在引用的段落末尾,例如引用[2]提到使用ConcurrentHashMap代替同步的WeakHashMap,所以要在相应位置加上[^2]。 另外,用户要求回结构清晰,逐步解决问题,所以需要分点说明,每个部分有明确的标题,如基本用法、最佳实践、常见问题等。同时,代码示例部分需要用正确的格式展示,比如Java代码块,帮助用户理解具体用法。 最后,生成的相关问题需要围绕ConcurrentHashMap展开,比如与HashMap的区别、线程安全机制、性能优化方法等,确保问题能引导用户深入理解该主题。 在检查过程中,要确保LaTeX语法正确,但这个问题可能不需要数学表达式,所以主要检查代码块和格式是否正确。同时,使用中文回,保持语言流畅自然,避免技术术语使用不当。</think>### JavaConcurrentHashMap的用法与最佳实践 #### 1. ConcurrentHashMap的基本概念 `ConcurrentHashMap`是Java并发包(`java.util.concurrent`)中的线程安全哈希表实现,支持高并发读写操作。它通过分段锁(Java 7)或CAS操作+节点锁(Java 8及以后)实现高效并发,避免了全局锁的性能瓶颈。 #### 2. 基本用法 ##### 初始化 ```java ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // 指定初始容量和负载因子 ConcurrentHashMap<String, Integer> map2 = new ConcurrentHashMap<>(16, 0.75f); ``` ##### 常用方法 ```java map.put("key", 100); // 线程安全的插入 int value = map.get("key"); // 线程安全的读取 map.computeIfAbsent("key", k -> 42); // 原子性操作:若不存在则计算并插入 map.forEach((k, v) -> System.out.println(k + ": " + v)); // 安全的遍历 ``` #### 3. 最佳实践 1. **减少锁竞争** - 合理设置`concurrencyLevel`(构造函数参数),默认值为16,需根据线程数调整。 - 使用`compute`、`merge`等原子方法替代“检查-写入”逻辑,避免显式同步。 2. **避免null值** `ConcurrentHashMap`不允许`null`作为键或值,需在代码中显式处理。 3. **弱一致性的迭代器** 遍历操作(如`entrySet().iterator()`)反映的是某一时刻的快照,不保证实时性。 4. **选择合适的并发版本** - Java 8+优化了锁粒度,优先使用新版本。 - 对于读多写少的场景,可结合`ReadWriteLock`或`ConcurrentHashMap`的原子操作。 5. **性能优化** ```java // 批量操作(并行阈值可调) map.search(1, (k, v) -> v > 100 ? k : null); map.reduce(1, (k, v) -> v, Integer::sum); ``` #### 4. 与HashMap、Hashtable的对比 | 特性 | ConcurrentHashMap | Hashtable | HashMap | |--------------------|-------------------|-----------------|-----------------| | 线程安全 | 是(分段锁/CAS) | 是(全局锁) | 否 | | 允许null键/值 | 否 | 否 | 是 | | 迭代器弱一致性 | 是 | 否 | 不适用 | #### 5. 常见问题 **Q: 如何保证复合操作的原子性?** 使用原子方法(如`compute`)或显式同步: ```java map.compute("counter", (k, v) -> v == null ? 1 : v + 1); ``` **Q: 多线程扩容是否会阻塞?** Java 8的`ConcurrentHashMap`通过多线程协同扩容,减少了阻塞时间。
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值