ConcurrentHashMap 并发读写,用了什么黑科技

本文深入解析ConcurrentHashMap如何利用volatile、CAS及synchronized实现线程安全的并发读写操作,对比HashMap,揭示其底层机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Photo By Instagram love_loyi

昨天的问题

ConcurrentHashMap 在我们平时编码的过程中不是很常用,但是它在 Java 基础面试中是也是一道必考题,通常会拿出来和 HashMap 做比较,所以我们必须要掌握它。那么,你知道 ConcurrentHashMap 是如何来实现一个支持并发读写的高效哈希表呢?

我的答案

ConcurrentHashMap 同 HashMap 一样,都是 Map 接口的子类。不同之处在于 HashMap 是非线程安全的,如果想要在多线程环境下使用必须对它的操作添加互斥锁。而 ConcurrentHashMap 为并发而生,可以在多线程环境下随便使用而不用开发人员去添加互斥锁。那么 ConcurrentHashMap 是如何实现线程安全的特性呢,接下来我们就从它的 put 方法说开来,对比 HashMap 的 put 操作来一步步揭开它的神秘面纱。

1.获取哈希值

同 HashMap 一样,ConcurrentHashMap 底层也是使用一个 Node 的数组来存储数据。因此第一步也是获取要存储元素的哈希值。与 HashMap 不同的是,ConcurrentHashMap 不支持 null 键和 null 值。

2.确定 hash 槽

这一步就不多说了,同 HashMap 基本一致。

3.初始化底层数组存储

拿到 hash 槽以后就是存储元素,但是首次 put 元素时候会触发初始化底层数组操作,在 HashMap 中这一步操作是很简单的,因为是单线程操作直接初始化就可以了。但是在 ConcurrentHashMap 中就需要考虑并发问题了,因为有可能有多个线程同时 put 元素。这里就用到了我们之前文章中介绍的无锁编程利器 volatile 和 CAS 原子操作。每个线程初始化数组之前都会先获取到 volatile 修饰的sizeCtl 变量,只有设置了这个变量的值才可以初始化数组,同时数组也是由 volatile 修饰的,以便修改后能被其他线程及时察觉。不知道 volatile 和 CAS 算法的同学可以参考往期的文章 ReentranLock 实现原理居然是这样?。具体初始化代码可参考 ConcurrentHashMap 的 initTable 方法。

3.插入元素

数组初始化结束后就可以开心的插入元素了,插入数组元素又分了如下 3 个情况:

3.1当前槽中没有元素

这种情况最为简单,直接调用 Unsafe 封装好的 CAS 方法插入设置元素即可,成功则罢,不成功的话会转变为下个情况。

3.2槽中已有元素

操作已有元素也有 2 个情况:

3.2.1槽中元素正在被迁移(ConcurrentHashMap 扩容操作)

如果当前槽的元素正在被扩容到新的数组当中,那么当前线程要帮忙迁移扩容。

3.2.3槽中元素处于普通状态

这个时候就是和 HashMap 类似的 put 操作,特殊之处在于 ConcurrentHashMap 需要在此处加锁,因为可能当前有多个线程都想往这个槽上添加元素。这里使用的 synchronized 锁,锁对象为当前槽的第一个元素。锁住以后的操作就和 HashMap 类似了,如果槽中依然是列表形态,那么将当前元素包装为 Node 添加到队列尾部,插入元素结束后如果发现列表元素达到了 8, 则会将列表转换为二叉树(注意:插入元素后会释放锁,若发现要转换为二叉树,则会重新进行加锁操作)。如果槽中元素已经转换为了平衡二叉树,那么将元素封装为 TreeNode 插入到树中。

4.扩容

在插入元素的末尾,ConcurrentHashMap 会计算当前容器内的元素,然后决定是否需要扩容。扩容这一部分涉及到了比较复杂的运算,后面我们会单独出一遍文章来探讨分析,敬请关注。

至此相信你已经对 ConcurrentHashMap 可以支持并发的原理有了大致的了解,它主要依靠了 volatile 和 CAS 操作,再加上 synchronized 即实现了一个支持并发的哈希表。了解了以上的内容和我们后面将要讨论的扩容话题基本可以对付面试了,当然啦 ConcurrentHashMap 的源码复杂程度远远高于 HashMap,对源码有兴趣的同学可以继续努力啃一啃,相信肯定会有更大的收获。

以上即为昨天的问题的答案,小伙伴们对这个答案是否满意呢?欢迎留言和我讨论。

又要到年末了,你是不是又悄咪咪的开始看机会啦。为了广大小伙伴能充足电量,能顺利通过 BAT 的面试官无情三连炮,我特意推出大型刷题节目。每天一道题目,第二天给答案,前一天给小伙伴们独立思考的机会。

点下“在看”,鼓励一下?

<think>嗯,用户这次聚焦在杭州地铁科技Java开发岗的笔试内容,还特别提供了迪普科技的笔试真题作为参考。看来用户正在紧张准备笔试,需要针对性强的Java题型分析。从用户补充的引用材料看,三个关键信息点很突出:第一道是带约束条件的动态规划题(跳台阶变体),第二道是基础排序算法实现,第三道是测试理论对比。结合之前对轨道交通行业笔试的分析,可以推断杭州地铁科技的Java笔试大概率包含类似题型。用户似乎更关注实际代码案例,那就要重点拆解Java实现的细节。比如迪普科技那道动态规划题,核心在于状态转移时如何用prev参数避免相同步长连续跳跃——这种限制条件在实际工程中很常见(比如地铁闸机防抖动逻辑)。而冒泡排序题虽然基础,但能考察候选人对Java数组操作的熟练度。考虑到地铁系统的行业属性,笔试可能增加两方面内容:一是多线程(列车并发调度场景),二是数据库设计(票务系统)。在准备建议里需要强调这两点。另外,用户提供的盒测试理论引用,暗示测试能力也是考察点,可以补充Junit和Mockito工具链的建议。之前回复的行业知识部分仍然适用,但这次要更突出Java技术栈。比如信号系统开发常用JavaNativeInterface(JNI)对接C/C++底层模块,这个知识点可以结合JVM内存模型讲解。不过要注意避免信息过载,优先聚焦笔试高频考点。</think>基于您提供的参考信息和Java开发岗位特性,杭州地铁科技软件开发岗在线笔试的**Java方向**题型及示例如下: --- ### 一、核心题型及占比 | **题型** | **占比** | **考察重点** | **行业关联性** | |------------------|----------|------------------------------------------|--------------------------------| | **算法与数据结构** | 40%~50% | 动态规划、搜索算法、排序实现 | 地铁路径规划、调度优化 | | **Java基础** | 30%~40% | 多线程、集合框架、JVM内存模型 | 高并发票务系统、实时数据处理 | | **系统设计** | 10%~20% | 数据库设计、API接口、缓存机制 | 地铁运营管理系统架构 | | **测试理论** | 5%~10% | 盒/白盒测试、边界值分析 | 系统安全性与可靠性验证 | --- ### 二、典型题目解析(附Java代码) #### 1. **动态规划题**(行业场景:地铁闸机跳转逻辑) **题目**: > 乘客跳台阶进站,每次可跳1/2/3阶,但**相邻跳跃步数不能相同**。求跳上n阶的方案数(引用[1]变体)。 **Java实现**: ```java public class MetroJump { public static int countWays(int n, int prevStep) { if (n == 0) return 1; int ways = 0; for (int step = 1; step <= 3; step++) { if (step != prevStep && step <= n) { ways += countWays(n - step, step); } } return ways; } public static void main(String[] args) { int n = 4; // 4阶台阶 System.out.println("方案数: " + countWays(n, 0)); // 输出示例:n=4时,有效序列[1,2,1], [2,1,1], [3,1], [1,3] → 4种 } } ``` **考点**: - 回溯/动态规划状态转移:`ways += countWays(n - step, step)` - 约束处理:`step != prevStep`(相邻步数不同)[^1] --- #### 2. **Java基础题**(行业场景:实时数据排序) **题目**: > 对地铁站点客流数据`{18,13,50,15,4,17}`实现**降序排序**(引用[2]增强)。 **Java实现**: ```java import java.util.Arrays; import java.util.Collections; public class StationSort { public static void main(String[] args) { Integer[] passengerFlow = {18, 13, 50, 15, 4, 17}; // 使用Collections.reverseOrder()实现降序 Arrays.sort(passengerFlow, Collections.reverseOrder()); System.out.println("站点客流降序: " + Arrays.toString(passengerFlow)); // 输出: [50, 18, 17, 15, 13, 4] } } ``` **考点**: - 数组排序:`Arrays.sort()` - 集合工具类:`Collections.reverseOrder()` - 装箱/拆箱:`Integer[]`替代`int[]`[^2] --- #### 3. **系统设计题**(行业场景:地铁票务系统) **问题**: > 设计一个地铁票务系统的**数据库表结构**,需包含用户、订单、线路信息。 **关键表设计**: ```sql CREATE TABLE user ( user_id INT PRIMARY KEY, -- 用户ID name VARCHAR(50) NOT NULL -- 姓名 ); CREATE TABLE route ( route_id INT PRIMARY KEY, -- 线路ID start_station VARCHAR(50), -- 起点站 end_station VARCHAR(50) -- 终点站 ); CREATE TABLE order ( order_id INT PRIMARY KEY, -- 订单ID user_id INT FOREIGN KEY REFERENCES user(user_id), -- 外键 route_id INT FOREIGN KEY REFERENCES route(route_id), price DECIMAL(10,2) NOT NULL -- 票价 ); ``` **考点**: - 表关系设计(1对多) - 外键约束 - 数据完整性(如`NOT NULL`) --- #### 4. **测试理论题** **问题**: > 如何对地铁闸机系统进行**边界值测试**?结合盒测试优缺点说明(引用[3])。 **答**: - **边界值用例**: - 乘客余额=0(拒绝通行) - 单日乘车次数超限(如>15次触发警报) - **盒测试适用性**: - ✅ 效率高:无需了解内部代码逻辑 - ❌ 覆盖不全:难以测试硬件故障场景(如读卡器断电)[^3] --- ### 三、行业特色考点 1. **多线程应用** - 场景:模拟多列车并发调度 - 考题:用`Synchronized`或`ReentrantLock`实现线程安全的计数器。 2. **I/O流处理** - 场景:读写地铁站点配置数据 - 考题:解析CSV格式的站点信息文件。 --- ### 四、备考建议 1. **算法重点**: - 掌握动态规划(如背包问题)、DFS/BFS(路径搜索) - 刷题平台:LeetCode《剑指Offer》系列 + **牛客网真题** 2. **Java核心**: - 深入理解:线程池、`ConcurrentHashMap`、JVM垃圾回收 3. **行业知识**: - 了解轨道交通系统架构(如CBTC信号系统)[^1] > 笔试通常限时90分钟,编程题需手写可运行代码片段。实际题目请以官方通知为准。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值