concurrenthashmap_一文看懂 jdk8 中的 ConcurrentHashMap

本文主要探讨JDK8中ConcurrentHashMap的实现原理,包括其摒弃分段锁采用的CAS和Synchronized机制,以及size、put、get等核心方法的实现。在高并发场景下,通过CounterCell数组和baseCount配合统计元素个数,避免频繁的CAS操作。put方法涉及的插入逻辑包括初始化、链表和红黑树的插入策略,以及扩容判断。同时,文章还介绍了helpTransfer和transfer方法在扩容过程中的作用。
摘要由CSDN通过智能技术生成

ConcurrentHashMap实现原理

ConcurrentHashMap 在 jdk7 升级j到 dk8之 后有较大的改动,jdk7 中主要采用 Segment 分段锁的思想,Segment 继承自ReentrantLock 类,依次来保证线程安全。限于篇幅原因,本文只讨论 jdk8 中的 ConcurrentHashMap 实现原理。有兴趣的同学可以自行研究 jdk7 中的实现。

jdk8 中的 ConcurrentHashMap 数据结构同 jdk8 中的 HashMap 数据结构一样,都是 数组+链表+红黑树。摒弃了 jdk7 中的分段锁设计,使用了 Node + CAS + Synchronized 来保证线程安全。

static class Node implements Map.Entry {
        final int hash;    final K key;    // volatile 修饰的变量可以保证线程可见性,同时也可以禁止指令重排序    // 有关 volatile 原理此处不展开,volatile 的实现原理可自行上网查阅    volatile V val;    volatile Node next;    //...}

重要方法实现原理

在探究方法实现之前,我们先认识一下 Unsafe 和 CAS 思想,ConcurrentHashMap 中大量用到 Unsafe 类和 CAS 思想。

Unsafe

Unsafe 是 jdk 提供的一个直接访问操作系统资源的工具类(底层c++实现),它可以直接分配内存,内存复制,copy,提供 cpu 级别的 CAS 乐观锁等操作。它的目的是为了增强java语言直接操作底层资源的能力。使用Unsafe类最主要的原因是避免使用高速缓冲区中的过期数据。

为了方便理解,举个栗子。类 User 有一个成员变量 name。我们new了一个对象 User 后,就知道了它在内存中的起始值 ,而成员变量 name 在对象中的位置偏移是固定的。这样通过这个起始值和这个偏移量就能够定位到 name 在内存中的具体位置。

Unsafe 提供了相应的方法获取静态成员变量,成员变量偏移量的方法,所以我们可以使用 Unsafe 直接更新内存中 name 的值。

CAS

CAS 译为 Compare And Swap,它是乐观锁的一种实现。假设内存值为 v,预期值为 e,想要更新成得值为 u,当且仅当内存值v等于预期值e时,才将v更新为u。CAS 操作不需要加锁,所以比起加锁的方式 CAS 效率更高。

size 方法

size 方法用于统计 map 中的元素个数,通过源码我们发现 size 核心是  sumCount  方法,其中变量 baseCount  的值是记录完成元素插入并且成功更新到 baseCount  上的元素个数,CounterCell 数组是记录完成元素插入但是在 CAS 修改 baseCount 失败时的元素个数,因此 baseCount + CounterCell 数组记录的总数是 map 中的元素个数。

这样设计的原因是高并发情况下大量的 CAS 修改 baseCount 的值是失败的,为了节约性能,CAS 更新 baseCount 失败之后用 CounterCell 数组记录下来,CounterCell 初始化数组长度为2,高并发情况可以扩容,每个数组节点分别记录落在当前数组的记录数,使用的是 CAS 去操作 value++,最后将所有节点的 value 求和,并加上 baseCount 的值,即为 map 元素个数。

public int size() {
        long n = sumCount();    return ((n < 0L) ? 0 :            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :            (int)n);}
final long sumCount() {
        // CounterCell 数组是修改 baseCount 失败的线程放入的数量    CounterCell[] as = counterCells; CounterCell a;    // baseCount 是修改 baseCount 成功的线程放入的数量    long sum = baseCount;    if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)                sum += a.value;        }    }    return sum;}
put 方法
put 方法是向map中存入元素,本质上调用了 putVal,该方法非常核心的方法之一,读者可结合笔者添加的注释加以理解
 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[] tab = table;;) {
             Node f; int n, i, fh;         // 如果 tab 未初始化,先初始化 tab,此处是懒加载的思想         if (tab == null || (n = tab.length) == 0)             tab = initTable();         // 如果计算出来的 tab 下标位置上没有其他元素,用 CAS 操作建立引用         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                 if (casTabAt(tab, i, null,                          new Node(hash, key, value, null)))                 break;                   // no lock when adding to empty bin         }         // 如果发现当前节点的哈希值是 MOVED,则说明正处于扩容状态中,当前线程加入扩容大军,帮助扩容         else if ((fh = f.hash) == MOVED)             tab = helpTransfer(tab, f);         else {
                 V oldVal = null;             // 哈希冲突,锁住当前节点             synchronized (f) {
                     if (tabAt(tab, i) == f) {
                         // fh>=0说明是链表,遍历寻找              
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值