ConcurrentHashMap源码分析之put、putVal

本文详细解析了ConcurrentHashMap的工作原理,包括其putVal方法的实现细节,如使用双检锁提高并发效率、通过计算hash值来分散数据、解决hash冲突的策略(链表与红黑树)以及自动扩容机制等。

说明

ConcurrentHashMap(以下简称CHM)的put直接调用putVal。putVal的实现有如下几个原则:

  1. CHM table(即Node<K,V>数组)的长度往往没有key的hash值大,为了尽可能分散数据,应当让key的hash值参与到index的计算中,所以将hash值的右移16位后和原值异或。
  2. 写入时用synchronized加锁,不过为了提高效率,采用双检锁
  3. 用的锁是table中的元素,链头或TreeBin
  4. 解决Hash冲突的方法有两种:冲突少用链表;冲突多用红黑树。阈值是8
  5. 如果有冲突,就检查是否需要扩容

源码

final V putVal(K key, V value, boolean onlyIfAbsent) {
 if (key == null || value == null) throw new NullPointerException();
 // key的hash值右移16位与原值“异或”,以扩大高16位对计算index的影响
 int hash = spread(key.hashCode());
 int binCount = 0;
 for (Node<K,V>[] tab = table;;) {
     Node<K,V> f; int n, i, fh;
     if (tab == null || (n = tab.length) == 0)
         tab = initTable();
     // (n-1)&hash体现了n为2的幂优势。‘&’可以消除非法hash的影响,跟spread里的“&0x7fff ffff”一起构成了双保险。
     else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
 	 // 目标位置未设置,则用CAS设置
     if (casTabAt(tab, i, null,
           new Node<K,V>(hash, key, value, null)))
         break;                   // no lock when adding to empty bin
     }
     // f是目标位置已有的Node。其hash值标为MOVED(-1),表示table已经扩容,且目标位置的Node已经复制到了nextTable。而且此时f的类型应该是:ForwardingNode
     else if ((fh = f.hash) == MOVED)
         // 直接将当前线程加入到扩容工作中。CHM的这个设计非常主动。
         tab = helpTransfer(tab, f);
     else {
         V oldVal = null;
         // 简单锁,以目标为指导的bin为锁
         synchronized (f) {
         	 // 判断一下是否被别的线程改掉了。典型的双检锁
             if (tabAt(tab, i) == f) {
                 // MOVED=-1,TREEBIN=-2,RESERVED=-3
                 if (fh >= 0) {
                     binCount = 1;
                     // 拉链循环,直到找到已有的Node(key相同),或链尾
                     for (Node<K,V> e = f;; ++binCount) {
                         K ek;
                         // 如果就是当前Node,直接设置
                         if (e.hash == hash &&
                             ((ek = e.key) == key ||
                              (ek != null && key.equals(ek)))) {
                             oldVal = e.val;
                             if (!onlyIfAbsent)
                                 e.val = value;
                             break;
                         }
                         // 直至链尾
                         Node<K,V> pred = e;
                         if ((e = e.next) == null) {
                             pred.next = new Node<K,V>(hash, key,
                                                       value, null);
                             break;
                         }
                     }
                 }
                 // 如果已经树化,则入树
                 else if (f instanceof TreeBin) {
                     Node<K,V> p;
                     // 树化时,table中的bin不是树根,而是指向树根,所以计为2。已经树化了,binCount也就不再增加。只是作为addCount的check参数,检查是否需要扩容
                     binCount = 2;
                     if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                    value)) != null) {
                         // oldVal不空,说明是覆盖
                         oldVal = p.val;
                         if (!onlyIfAbsent)
                             p.val = value;
                     }
                 }
             }
         }
         if (binCount != 0) {
             // 当拉链长度超过8时,树化该链
             if (binCount >= TREEIFY_THRESHOLD)
                 treeifyBin(tab, i);
             // 如果是覆盖旧值,则无需扩容
             if (oldVal != null)
                 return oldVal;
             break;
         }
     }
 }
 // 对于新增Node的情况(原处无值),binCount>0时执行扩容
 addCount(1L, binCount);
 return null;
}
<think>我们正在讨论ConcurrentHashMap的底层实现原理,特别是在JDK1.8中的实现。根据引用内容,我们可以总结出以下关键点: 1. **数据结构**:数组+链表+红黑树。这与HashMap类似,但增加了并发控制。 2. **线程安全机制**:使用CAS(Compare and Swap)操作和synchronized关键字来实现细粒度的锁。 3. **重要概念**:桶(数组中的每个位置),链表转红黑树的阈值(默认8),红黑树退化为链表的阈值(默认6)。 4. **操作原理**:put、get等操作的具体流程。 接下来,我们将详细解释ConcurrentHashMap在JDK1.8中的底层实现原理。 ### 1. 数据结构 在JDK1.8中,ConcurrentHashMap的底层数据结构是一个`Node<K,V>[]`数组。每个`Node`可以是一个链表节点,也可以是一棵红黑树的根节点。当链表长度超过阈值(默认8)时,链表会转换为红黑树;当红黑树的节点数小于阈值(默认6)时,红黑树会退化为链表。这种设计是为了在哈希冲突严重时提高查询效率。 ### 2. 线程安全机制 ConcurrentHashMap通过以下方式实现线程安全: - **CAS操作**:用于无锁化的插入操作(例如初始化数组、插入节点到空桶等)。 - **synchronized锁**:当需要操作链表或红黑树时,只锁住当前桶(即数组的一个元素),这样其他桶的操作不受影响,实现了更细粒度的并发控制。 ### 3. put操作流程 1. **计算hash值**:根据key计算hash值,定位到数组中的桶。 2. **初始化数组**:如果数组还未初始化,则通过CAS操作初始化数组。 3. **插入节点**: - 如果桶为空,则尝试用CAS操作将新节点插入到空桶中。 - 如果桶不为空,则使用`synchronized`锁住当前桶(即链表头节点或红黑树根节点)。 - 如果是链表,则遍历链表,如果找到key相同的节点则更新值,否则在链表尾部插入新节点。插入后检查链表长度是否超过阈值,若超过则转换为红黑树。 - 如果是红黑树,则按照红黑树的方式插入节点。 4. **统计元素个数**:使用`CounterCell`数组来统计元素个数,避免单一计数器的竞争。 ### 4. get操作流程 get操作不需要加锁,因为Node的val和next都是用volatile修饰的,保证了可见性。 1. 计算hash值,定位到桶。 2. 如果桶中第一个节点就是目标节点,直接返回。 3. 如果桶中是链表,则遍历链表查找。 4. 如果桶中是红黑树,则调用红黑树的查找方法。 ### 5. 扩容机制 当数组容量达到阈值时,会触发扩容。ConcurrentHashMap支持多线程同时扩容,每个线程负责一部分桶的迁移。在迁移过程中,get操作可以正常进行(因为旧数组还在使用),而put操作在遇到正在迁移的桶时会协助迁移。 ### 示例代码分析 以下是一个简化的put操作流程(基于JDK1.8源码): ```java final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) tab = initTable(); // 初始化数组 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 桶为空,尝试CAS插入 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // 插入成功 } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 协助扩容 else { V oldVal = null; synchronized (f) { // 锁住桶头节点 if (tabAt(tab, i) == f) { if (fh >= 0) { // 链表 binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { // 红黑树 Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); // 链表转红黑树 if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); // 更新计数 return null; } ``` ### 总结 ConcurrentHashMap在JDK1.8中通过数组+链表+红黑树的数据结构,结合CAS和synchronized(锁桶)实现了高效的并发操作。它通过细粒度的锁策略和扩容时多线程协助机制,大大提高了并发性能。 §§相关问题§§ 1. ConcurrentHashMap在JDK1.8中如何保证线程安全? 2. ConcurrentHashMap的扩容机制是如何工作的? 3. 为什么ConcurrentHashMap在链表长度超过8时转换为红黑树? 4. ConcurrentHashMap的get操作为什么不需要加锁?</think>### ConcurrentHashMap 底层实现原理(JDK 1.8) #### 1. **核心数据结构** ConcurrentHashMap 在 JDK 1.8 中采用 **数组 + 链表 + 红黑树** 的复合结构: - **数组**(`Node<K,V>[] table`):作为哈希桶的容器,每个桶称为一个 "Bin"。 - **链表**:解决哈希冲突,当桶中元素较少时使用。 - **红黑树**:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转换为红黑树(查询复杂度从 $O(n)$ 优化为 $O(\log n)$);当节点数 ≤ 6 时退化为链表[^3]。 #### 2. **线程安全机制** - **CAS(Compare And Swap)**:用于无锁化的原子操作(如数组初始化、节点插入)。 - **Synchronized 锁细化**:只锁住当前操作的桶(链表头节点或红黑树根节点),而非整个表,实现高并发[^3]。 - **Volatile 变量**:节点值(`val`)和下一个节点(`next`)用 `volatile` 修饰,保证可见性。 #### 3. **关键操作原理** ##### **put 操作流程** 1. **计算哈希**:根据 key 的哈希值定位桶位置。 ```java int hash = spread(key.hashCode()); int i = (table.length - 1) & hash; // 定位桶下标 ``` 2. **桶初始化**: - 若桶为空,通过 CAS 插入新节点(无需加锁)。 3. **哈希冲突处理**: - 若桶非空,用 `synchronized` 锁住桶的头节点: - **链表**:遍历链表,更新或插入新节点。 - **红黑树**:通过 `TreeBin` 操作红黑树。 4. **扩容检查**:若元素总数超过阈值(容量 × 负载因子),触发扩容[^3]。 ##### **get 操作流程** 1. 根据 key 的哈希值定位桶位置。 2. 若桶头节点匹配,直接返回值。 3. 若桶为红黑树,调用 `TreeBin.find()` 查询。 4. 若桶为链表,遍历链表查询。 > **无需加锁**:依赖 `volatile` 保证可见性,且读操作不阻塞[^4]。 #### 4. **扩容机制(多线程协同)** - **触发条件**:元素数量 ≥ 容量 × 负载因子(默认 0.75)。 - **多线程协作**: - 线程在操作时发现桶处于迁移状态(`ForwardingNode`),会协助迁移其他桶。 - 迁移时旧桶锁住,新桶可继续读写,实现无阻塞查询[^3]。 #### 5. **源码关键设计** - **节点类型**: - `Node`:基础链表节点。 - `TreeBin`:红黑树管理器(封装根节点)。 - `ForwardingNode`:扩容占位节点(指向新数组)。 - **并发控制**: - `sizeCtl`:控制数组初始化和扩容的 volatile 变量。 - `CounterCell`:分片计数(避免 CAS 竞争)。 #### 6. **与 JDK 1.7 的区别** | 特性 | JDK 1.7 | JDK 1.8 | |--------------|----------------------------------|----------------------------------| | **数据结构** | Segment 分段锁 + 链表 | 数组 + 链表 + 红黑树 + 细粒度锁 | | **锁粒度** | 锁住整个 Segment(含多个桶) | 仅锁单个桶 | | **并发度** | 由 Segment 数量固定 | 锁粒度更细,并发性能更高 | ### 总结 JDK 1.8 的 ConcurrentHashMap 通过 **CAS + Synchronized 锁细化 + 红黑树优化**,在保证线程安全的同时显著提升并发性能。其设计核心是: 1. 细粒度锁(桶级别锁) 2. 无锁化读操作 3. 多线程协同扩容 4. 动态链表/树转换 > 示例:插入元素时仅锁一个桶 > ```java > synchronized (f) { // f 是桶的头节点 > if (tabAt(tab, i) == f) { > // 操作链表或红黑树 > } > } > ``` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值