Java基础 -> ConcurrentHashMap的实现原理

ConcurrentHashMap的实现原理

在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会

ConcurrentHashMap所使用的锁分段技术。

  1. 首先将数据分成一段一段地存储
  2. 然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

jdk1.7

了解了解就行,现在jdk大多都是1.8,几乎重写了一边

采用Segment + HashEntry的方式进行实现,结构如下:

img

  • ConcurrentHashMap是由Segment数组结构HashEntry数组结构(类似Map)组成。

  • Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。

  • 一个ConcurrentHashMap里包含一个Segment数组。

  • Segment的结构和HashMap类似,是一种数组和链表结构。

  • Segment数组的每个节点都相当于一个小HashMap,这样就可以对单个Segment节点加锁,不会影响到所有数据都被锁上

  • 原理跟HashMap差不多,都是用hash算法找位置

    • 第一次hash找Segment数组的某个节点
    • 第二次hash找这个Segment节点下这个小HashMap的数组位置(就类似HashMap的操作了)

jdk1.8

ConcurrentHashMap在1.8中的实现,相比于1.7的版本基本上全部都变掉了。

  1. 首先,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构(回到了HashMap结构)。
  2. 而对于锁的粒度,调整为对每个数组元素加锁(就是对每个链表或红黑树加锁)
  3. ConcurrentHashMap采用 CAS + Synchronized来保证并发安全进行实现(乐观锁+悲观锁)

img

ConcurrentHashMap的put实现

  • 与HashMap稍微不同就是,HashMap会计算hash和equals()比较后直接加上去

  • ConcurrentHashMap会有相应锁的控制(CAS+synchronized)后再添加数据

    • 执行第一次put方法时,采用CAS机制
    • 链表or树的其他节点put就是用synchronized加锁,利用头节点锁住整个链表或者树结构
    • 后面跟HashMap差不多了

    为什么不都用CAS机制,而是有一部分用synchronized呢?

    • 还是因为CAS的缺点,不能对多个变量进行原子操作,不能保证代码块的原子性
    • 并且CAS的预测值与实际值比较并不包括next的属性,仅仅对(k,v)进行比较
    • 所以对同一节点进行操作时
      • 线程1要修改value属性,线程2修改next属性
      • 线程2执行快,先替换了next属性
      • 而线程1预测(k,v)并没有变化,又将之前的旧next覆盖掉线程2修改后的

    大帅比,点赞了没?

<think>嗯,用户想了解ConcurrentHashMap实现原理,特别是Java并发容器和源码解析方面的内容。首先,我需要回顾一下自己关于ConcurrentHashMap的知识,确保信息准确。ConcurrentHashMapJava中是一个非常重要的线程安全的哈希表实现,用于多线程环境下的高效操作。不过,用户提供的引用资料里提到JDK1.5和1.8的不同实现,需要区分这两个版本的差异,因为实现原理在1.8之后有了很大的变化。 首先,用户提到的引用[4]提到,JDK1.5时期ConcurrentHashMap使用了分段锁(Segment),而JDK1.8之后改用了CAS和synchronized优化。这应该是关键点。因此,在回答时需要分版本讲解,但用户可能主要关注最新的实现,也就是1.8之后的版本,但可能需要对比说明。 接下来,我需要确保回答结构清晰,按照用户的要求分步骤解析。根据用户的示例,回答应该包括原理、源码结构、成员变量、核心方法分析等部分。同时,需要引用用户提供的引用资料中的内容,比如引用[1]、[2]、[3]、[4],但要注意这些引用是否准确对应到内容中。 比如,引用[1]提到ConcurrentHashMap是线程安全的HashMap,这可能需要在介绍时引用。引用[2]说明ConcurrentHashMap继承了AbstractMap并实现了ConcurrentMap接口,这部分应该在类结构里提到。引用[3]提到Segment的数据结构,但需要注意这是在1.8之前的版本,所以需要说明版本差异。引用[4]详细描述了1.5和1.8的实现变化,特别是分段锁到CAS和synchronized的转变,这里需要重点展开,并可能作为优化部分的内容。 接下来,我需要组织内容的结构。可能分为: 1. 实现原理概述:分段锁(JDK1.7)和CAS+synchronized(JDK1.8)的区别,强调1.8的改进。 2. 类结构与成员变量:继承关系、关键变量如Node数组、sizeCtl等。 3. 核心方法分析:如putVal、initTable、transfer(扩容)等,结合源码解析。 4. 线程安全机制:CAS、volatile变量、synchronized的使用。 5. 性能优化点:如链表转红黑树、扩容协助等。 需要注意使用正确的LaTeX格式,如行内公式用$...$,独立公式用$$...$$,但可能在这个问题中数学公式较少,不过如果有涉及计算或算法复杂度的地方需要正确使用。例如,提到时间复杂度时可能需要用$O(\log n)$这样的表达。 另外,用户要求生成相关问题,可能需要根据内容提出相关问题,比如JDK1.8的改进、CAS机制在ConcurrentHashMap中的应用、红黑树的作用等。 需要检查引用的正确性,比如引用[4]提到JDK1.8使用Unsafe自旋操作,而ConcurrentHashMap在1.8中确实摒弃了分段锁,改为每个桶节点独立加锁,结合CAS和synchronized。例如,put方法中通过CAS尝试无锁插入,失败后再synchronized锁定链表头或树节点。 在源码解析部分,可能需要摘录关键代码片段,比如putVal方法中的循环结构、CAS设置节点,以及initTable方法中的sizeCtl变量的使用。同时,注意代码块的格式,用三个反引号包围,并注明语言为Java。 另外,需要解释成员变量的作用,如sizeCtl的不同取值代表的不同状态(初始化、扩容中),以及如何通过volatile变量保证可见性。例如,Node数组用volatile修饰,确保扩容时其他线程能立即看到变化。 在性能分析部分,要比较分段锁和CAS+synchronized的优劣,比如减少锁粒度,降低竞争,提高并发度。同时,链表转红黑树(当链表长度超过8时)如何避免哈希碰撞导致的性能下降。 可能还需要提及并发扩容机制,即多个线程可以协助扩容,通过transfer方法分片迁移数据,提高效率。 最后,生成的相关问题需要围绕实现原理、源码细节、版本变化、性能优化等方面,确保问题具有针对性,帮助用户进一步深入学习。</think>### ConcurrentHashMap 实现原理分析 #### 一、核心设计思想 在JDK1.8中,ConcurrentHashMap摒弃了早期的**分段锁(Segment)**设计[^4],改为基于`CAS`+`synchronized`的细粒度锁机制。其核心数据结构与HashMap类似,采用**数组+链表/红黑树**实现,通过以下方式保证线程安全: 1. 对链表头节点使用`synchronized`加锁 2. 通过`CAS`操作控制数组扩容、节点插入等并发操作 3. `volatile`修饰成员变量保证内存可见性 #### 二、关键源码结构 ```java public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { transient volatile Node<K,V>[] table; // 哈希表数组 private transient volatile int sizeCtl; // 控制表初始化和扩容的标志 static class Node<K,V> implements Map.Entry<K,V> { // 基础节点 final int hash; final K key; volatile V val; volatile Node<K,V> next; } static final class TreeNode<K,V> extends Node<K,V> { // 红黑树节点 TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; } } ``` 类继承关系满足`AbstractMap -> ConcurrentMap`的规范[^2],`sizeCtl`的不同取值对应不同状态: - `-1`:表示正在初始化 - `-N`:表示有`N-1`个线程正在扩容 - 正数:表示扩容阈值或初始容量 #### 三、核心方法解析 ##### 1. putVal() 方法实现 ```java final V putVal(K key, V value, boolean onlyIfAbsent) { for (Node<K,V>[] tab;;) { // 通过CAS尝试无锁插入 if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) break; // CAS插入成功则退出循环 } else { synchronized (f) { // 对链表头加锁 // 处理链表/红黑树插入逻辑 if (tabAt(tab, i) == f) { if (fh >= 0) { // 链表插入逻辑 } else if (f instanceof TreeBin) { // 红黑树插入逻辑 } } } } } addCount(1L, binCount); return null; } ``` 通过`tabAt()`获取数组元素时使用`Unsafe.getObjectVolatile`保证可见性,`casTabAt()`使用`Unsafe.compareAndSwapObject`实现原子更新。 ##### 2. 初始化哈希表 ```java private final Node<K,V>[] initTable() { while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // 其他线程正在初始化 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 执行实际的数组初始化 } finally { sizeCtl = sc; } break; } } return tab; } ``` 通过`CAS`保证只有一个线程执行初始化操作。 #### 四、线程安全机制对比 | 版本 | 锁粒度 | 并发度 | 实现复杂度 | |------|--------|--------|------------| | JDK1.7 | 段锁(默认16段) | 段级并发 | 中等 | | JDK1.8 | 桶级锁(链表头节点) | 节点级并发 | 较高 | JDK1.8通过以下优化提升性能: 1. 链表长度超过8时转换为红黑树(时间复杂度从$O(n)$降为$O(\log n)$) 2. 多线程协同扩容(`transfer()`方法) 3. 使用`CounterCell`消除`size()`统计的竞争 #### 五、典型应用场景 1. 高并发缓存系统(如Guava Cache底层实现) 2. 实时计算中的状态存储 3. 多线程共享配置存储
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值