ConcurrentHashMap
实现原理
JDK1.7:
JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。

JDK1.8:
JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用 CAS + synchronized实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

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

先定位到相应的 Segment ,然后再进行 put 操作。
首先会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。
- 尝试自旋获取锁。
- 如果重试的次数达到了
MAX_SCAN_RETRIES则改为阻塞锁获取,保证能获取成功。
JDK1.8:

大致可以分为以下步骤:
- 根据 key 计算出 hash 值;
- 判断是否需要进行初始化;
-
定位到 Node,拿到首节点 f,判断首节点 f:
- 如果为 null ,则通过 CAS 的方式尝试添加;
- 如果为
f.hash = MOVED = -1,说明其他线程在扩容,参与一起扩容; - 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;
- 当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树。
ConcurrentHashMap 的 get 方法?
JDK1.7:

首先,根据 key 计算出 hash 值定位到具体的 Segment ,再根据 hash 值获取定位 HashEntry 对象,并对 HashEntry 对象进行链表遍历,找到对应元素。
由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以每次获取时都是最新值。
JDK1.8:

大致可以分为以下步骤:
- 根据 key 计算出 hash 值,判断数组是否为空;
- 如果是首节点,就直接返回;
- 如果是红黑树结构,就从红黑树里面查询;
- 如果是链表结构,循环遍历判断。
Q:ConcurrentHashMap 的 get 方法是否要加锁,为什么?
get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。
ConcurrentHashMap 的 remove方法?
JDK1.7:

JDK1.8:

ConcurrentHashMap 的 扩容方法?
JDK1.7:

JDK1.8:

ConcurrentHashMap 的 size方法?
JDK1.7:

JDK1.8:

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在JDK1.7和1.8中的实现原理和操作方法,包括put、get、remove和扩容等。JDK1.7使用Segment分段锁,而JDK1.8选择节点级别的锁,并使用了红黑树优化,降低了查询时间复杂度。
670

被折叠的 条评论
为什么被折叠?



