ConcurrentHashMap逻辑整理(一)

本文详细解析了ConcurrentHashMap的内部工作原理,包括其数组全局唯一性、懒加载模式、线程安全的put操作、扩容流程、协助扩容以及高效的查询策略,特别强调了红黑树的使用和维护平衡的重要性。

ConcurrentHashMap数组全局唯一,采用懒加载模式初始化。
一般项目中是用ConcurrentHashMap,都是作为JVM缓存使用的
key和value都不能为null

put操作:

  1. 将key进行hash运算,计算出hash值,然后基于这个hash值确定数据存放的索引位置
  2. 查看COncurrentHashMap的数组是否已经初始化,(数组全局唯一,懒加载,DCL保证线程安全)
    – 初始化就继续尝试存放数据
    – 没初始化,先尝试将数组new出来
  3. 基于hash值,确定数据存放位置
    – 如果数组索引位置没有数据,将当前数据扔进去。有限将key-value封装为一个Node对象,这里为了保证线程安全,直接采用CAS的方式将当前索引位置的null替换为指定的Node数据
    如果CAS成功,循环结束
    如果CAS失败,重走循环,从第2步开始
  4. 如果数组索引位置有数据,查看当前数组的状态是否是扩容的情况 MOVED(hash=-1)
    – 如果正在扩容,单签线程去帮助扩容,加快扩容速度,协助扩容结束后,重新走循环
  5. 如果数组索引位置有数据,且没有在扩容影响到当前线程
    – 加锁,基于数组索引位置的Node对象作为synchronized的锁
    – 循环将数据挂到链表的末端
    数据添加到链表后,查看链表长度是否达到了8,达到了8个,就需要尝试链表转红黑树(ConcurrentHashMap要求必须数组长度 >= 64 且 链表长度达到8才会转换,如果数组长度未达到64,会有限扩容数组)
    – 将数据添加到红黑树结构中 TREEBIN(hash=-2)
  6. 循环结束
  7. 需要记录元素个数,为了保证线程安全,这里采用LongAdder做计数器。(LongAdder比ActomicLong的效率高)
  8. 添加结束

扩容流程&协助扩容

ConcurrentHashMap扩容只有两件事:

  • 构建新数组,长度是老数组长度的2倍(new)
  • 将老数组中的数据迁移到新数组中

有一个属性可以区分扩容线程和协助扩容线程

private transient volatile int sizeCtl;

第一个来扩容的线程,会优先修改sizeCtl的值,将其修改为一个小于-1的值。
其他所有来协助扩容的线程,发现sizeCtl小于-1,直接对sizeCtl做 +1 操作,代表我来扩容了。修改sizeCtl的操作是基于CAS的。

  • 扩容的线程是来初始化新数组的,并且做迁移数据的操作。
  • 协助扩容的线程是来帮忙进行数据迁移的,不会做数组初始化。

整体扩容流程

  1. 计算每次迁移多长索引位置的数据到新数组(步长,将老数组数据分成n段,每次迁移一段),会根据CPU内核数和数组长度来计算,最小是16。
  2. 会由扩容线程。来初始化新数组,长度是原来的2倍。
  3. 扩容线程和协助扩容线程开始领取迁移任务。领取到任务的准备干活,没领取到任务的可以退出扩容。
  4. 没领取到任务的线程可以退出扩容,但是退出前,查看下扩容结束了没
    结束了,说明当前线程是最后一个退出扩容的线程,整体再检查一遍。
    没结束,sizeCtl-1,退出
  5. 迁移数据操作,在迁移任务中某一个索引位置是,会由不同情况
    当前位置没有数据,直接扔一个MOVED(hash = -1)
    当前索引位置是moved,啥也不做。
    当前位置有数据,锁住当前位置
    链表迁移到新数组,
    红黑树迁移到新数组(红黑树保留一个双向链表,利用双向链表迁移数据)

查询

ConcurrentHashMap本来就是作为JVM缓存使用的,对查询速度要求极高,所以查询永远不会泽色,无论什么情况,都能去查询数据。

查询数据的操作有以下几种情况:

  • 数据在数组上,查询到即返回
  • 数据在链表上,next遍历查找,找到返回
  • 数据在红黑树上
    • 如果此时有写线程在操作红黑树或者等待操作红黑树,那么读线程会去查询保留的双向链表
    • 如果此时没有写线程在操作或等待操作红黑树,直接去红黑树中找数据。如果此时有写线程想操作红黑树,那么需要等到读线程完毕后,才可以操作。
  • 如果数组上的Node是MOVED状态,代表数据已经迁移到新数组上了,直接去新数组上去查询数据
  • 如果数组上的Node是REVERSED状态,代表当前位置被占了,但是value还在计算中,返回null

红黑树是一个平衡的二叉树,怎么保持平衡的?
为了保证平衡,写操作可能会旋转某个节点,导致节点的指针发生变化。如果此时读线程在红黑树中遍历找数据,结果写线程改变了红黑树的指针,导致无法找到对应的数据。

有一个属性标识该树节点正在被读或者被写入

volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock,每次加4,>= 4 就是正在被读取
<think>嗯,用户这次明确询问ConcurrentHashMap的常用方法,需要结合提供的多个引用资料整理核心API。用户应该是个Java开发者,可能在设计并发程序时需要线程安全的Map实现。 从引用内容看,需要重点突出几个特性:首先是线程安全的实现机制(引用5提到CAS+synchronized的桶级锁),其次是迭代时的弱致性特性(引用3的遍历删除示例很有说服力),还要说明与HashMap的关键区别(引用5的对比表格很清晰)。 引用1的综述提到"添加、删除、获取、检查存在"等基础方法,引用5则补充了computeIfAbsent等原子方法。特别要注意初始化机制——引用5强调的懒加载和sizeCtl变量值得说明。对于size()方法,引用4指出不同JDK版本的实现差异,但用户问的是常用方法,这部分可以简略带过。 用户给的代码示例(引用3)展示遍历时删除的安全特性,这个实际案例可以融入解释。考虑到用户可能正在处理高并发场景,最后补充适用场景(引用5的缓存/计数器)会更有帮助。 需要避免深入源码细节(如引用4的size原理分析),重点放在方法用途和线程安全特性上。数学公式不适用本问题,但要注意所有代码块用$...$包裹行内代码,比如$conMap.put()$这样的写法。 </think>以下是 `ConcurrentHashMap` 的常用方法及关键特性总结,结合线程安全设计和适用场景: --- ### **、核心方法** 1. **元素操作** - `put(K key, V value)` 线程安全地插入键值对(桶级别锁 + CAS)[^5]。 - `get(Object key)` 无锁读取,保证弱致性[^3]。 - `remove(Object key)` 并发删除元素,支持遍历时删除(见示例代码)[^3]。 - `computeIfAbsent(K key, Function mappingFunction)` 原子操作:若 key 不存在,自动计算并插入值(避免重复计算)[^5]。 2. **状态检查** - `containsKey(Object key)` 检查 key 是否存在(无锁实现)[^1]。 - `isEmpty()` 判断 Map 是否为空(基于快照计数)[^4]。 3. **批量操作** - `forEach(BiConsumer<? super K, ? super V> action)` 并行遍历(弱致性视图)[^1]。 - `reduce()` / `search()` 支持聚合操作(如求和、查找)[^1]。 --- ### **二、关键特性** 1. **线程安全机制** - **JDK 1.8+**:桶级别锁(`synchronized` 锁链表头节点/红黑树根节点) + CAS 操作[^5]。 - **弱致性迭代器**:遍历时可安全增删元素(与 `HashMap` 的 fail-fast 不同)[^3]: ```java for (Entry<Long, String> entry : conMap.entrySet()) { if (entry.getKey() < 10) { conMap.remove(entry.getKey()); // 遍历中删除安全 } } ``` 2. **初始化与扩容** - **懒加载**:首次插入时初始化数组(`initTable`)[^5]。 - **扩容控制**:`sizeCtl` 变量标记状态(-1 表示初始化中)[^5]。 3. **特殊限制** - **不支持 `null` 键值**:避免并发歧义(`HashMap` 允许)[^5]。 --- ### **三、与 `HashMap` 的区别** | **特性** | `ConcurrentHashMap` | `HashMap` | |------------------|--------------------------|-----------------| | **线程安全** | 是(CAS + 桶锁) | 否 | | **锁粒度** | 桶级别 | 无锁 | | **迭代致性** | 弱致性 | fail-fast | | `null` 键值支持 | ❌ 不支持 | ✅ 支持 | --- ### **四、适用场景** - **高并发读写**:如缓存、计数器[^5]。 - **原子复合操作**:需保证线程安全的组合逻辑(如 `computeIfAbsent`)[^2]。 - **实时数据遍历**:允许遍历时动态修改数据(如流处理)[^3]。 --- ### 示例代码:线程安全计数 ```java ConcurrentHashMap<String, AtomicInteger> counterMap = new ConcurrentHashMap<>(); counterMap.computeIfAbsent("key", k -> new AtomicInteger(0)).incrementAndGet(); ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值