ConcurrentHashMap 源码分析(一)

本文详细解读了ConcurrentHashMap的put()方法源码,重点剖析了空值检查、哈希值计算、链表/红黑树插入、Hash冲突处理以及helpTransfer()方法的工作原理,展示了多线程环境下并发数据结构的高效管理。

一、简述

本文对 ConcurrentHashMap#put() 源码进行分析。

二、源码概览

public V put(K key, V value) {
    return putVal(key, value, false);
}

上面是 ConcurrentHashMap#put() 的源码,我们可以看出其核心逻辑在 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<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        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;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
        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);
                                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;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

上面是 ConcurrentHashMap#putVal() 的源码,有兴趣的小伙伴可以先试着读一下。

三、核心流程分析

final V putVal(K key, V value, boolean onlyIfAbsent) {
    
    // 检查 key 和 value 是否为 null,如果是则抛出 NullPointerException 异常
    if (key == null || value == null) throw new NullPointerException();
    
    // 调用 spread 方法将 key 的哈希码进行扩散,得到一个散列值 hash
    int hash = spread(key.hashCode());
    int binCount = 0;

    // 开启循环
    for (Node<K,V>[] tab = table;;) {
        // 定义一些变量
        Node<K,V> f; int n, i, fh; K fk; V fv;

        // 检查当前的哈希表(tab)是否为空或长度为 0
        if (tab == null || (n = tab.length) == 0)
            // 调用 initTable() 方法初始化哈希表
            tab = initTable();
            
        // 如果当前槽位(f = tabAt(tab, i = (n - 1) & hash))为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 使用 CAS 操作尝试在该槽位上添加新的节点
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                // 成功则跳出循环
                break;                   // no lock when adding to empty bin
        }
            
        // 如果当前槽位的哈希值为 MOVED
        else if ((fh = f.hash) == MOVED)
            // 帮助其进行哈希表的转移操作
            tab = helpTransfer(tab, f);
            
        // 如果 onlyIfAbsent 为 true,并且当前槽位的键与要添加的键相同
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            // 直接返回当前槽位的值
            return fv;
            
        else {
            V oldVal = null;
            // 对当前槽位的节点进行加锁
            synchronized (f) {
                // 暂时省略 synchronized 中的内容
            }

            // 检查 binCount 是否不为 0
            if (binCount != 0) {
                // 如果 binCount 的值大于或等于 TREEIFY_THRESHOLD(默认值为8)
                if (binCount >= TREEIFY_THRESHOLD)
                    // 将当前的链表结构转化为红黑树结构 
                    treeifyBin(tab, i);
                // 如果 oldVal 不为 null
                if (oldVal != null)
                    // 直接返回 oldVal
                    return oldVal;
                // 跳出循环
                break;
            }
        }
    }
    // 调用 addCount 方法增加元素的数量
    addCount(1L, binCount);
    return null;
}

从上面的源码中可以分析出,ConcurrentHashMap#putVal() 的核心逻辑为:

在这里插入图片描述

  1. 首先进行空值检查,如果键或值为 null,那么抛出 NullPointerException
  2. 使用 spread() 方法,计算 Hash 值
  3. 开启循环,首先检测 Hash 表的状态是否已完成初始化
  4. 未完成初始化,使用 initTable() 方法完成初始化
  5. 若 Hash 表已完成初始化,则检查需要插入的槽位是否为空
  6. 若槽位为空,则采用 CAS 插入新节点,新节点插入成功退出循环
  7. 若槽位不为空,判断插入槽位是否需要移动
  8. 若需要移动,使用 helpTransfer() 方法实现槽位转移(扩容)
  9. 若不需要移动,则检查当前槽位的键是否与插入的键相同
  10. 若键相同,直接返回当前槽位的值,退出循环
  11. 若键不同,发生 Hash 冲突,进入 synchronized 代码块执行解决 Hash 冲突的逻辑

四、Hash 冲突流程分析

// 之前 synchronized 省略的内容
// 对哈希桶的节点加锁
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);
                    break;
                }
            }
        }
        // 如果当前节点是红黑树节点
        else if (f instanceof TreeBin) {
            Node<K,V> p;
            binCount = 2;
            // 调用 putTreeVal() 方法
            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                  value)) != null) {
                oldVal = p.val;
                if (!onlyIfAbsent)
                    // 如果键已存在则更新旧值
                    p.val = value;
            }
        }
        // 如果当前节点是 ReservationNode
        else if (f instanceof ReservationNode)
            // 抛出 IllegalStateException 异常
            throw new IllegalStateException("Recursive update");
    }
}

上面是 ConcurrentHashMap#putVal() 方法中发生 Hash 冲突时的源码。

在这里插入图片描述

  1. 首先,将 Hash 桶中的节点加 synchronized
  2. 判断槽位是否改变
  3. 槽位未改变,检查是否为链表节点
  4. 若是链表节点,则遍历链表,键已存在则更新值,键不存在则新增节点
  5. 若是红黑树节点,则调用红黑树的 putTreeVal() 方法,键已存在则更新值,键不存在则新增节点
  6. 若是 ReservationNode 则抛出 IllegalStateException 异常

五、FAQ

5.1 helpTransfer() 方法是什么

helpTransfer() 方法的主要工作是检查是否满足扩容条件,如果满足,则协助进行扩容操作。具体来说,它会检查当前的哈希表是否正在进行扩容操作,如果是,则帮助完成扩容;如果不是,则直接返回当前的哈希表。

5.2 Hash 冲突源码中为什么需要判断槽位是否改变

if (tabAt(tab, i) == f) 的目的是为了检查在进入同步块之后,当前槽位的节点是否发生了变化。

在多线程环境下,当一个线程获取到锁并进入同步块时,其他线程可能已经修改了哈希表的状态。因此,在进行节点操作之前,需要再次检查当前槽位的节点是否与预期的节点相同。

如果当前槽位的节点与预期的节点不同,那么说明在这个线程获取锁的过程中,其他线程已经修改了哈希表的状态。在这种情况下,当前线程应该跳过后续的操作,因为它们可能基于错误的状态。
这是一种常见的并发编程技巧,被称为"双重检查锁"(Double-Checked Locking)。它可以确保在多线程环境下的正确性和效率。

5.3 ReservationNode 是什么

在 ConcurrentHashMap 中,ReservationNode 是一个特殊类型的节点,是一个临时的占位符,不应该出现在正常的操作中。如果出现了,那么可能是发生了递归更新

往期推荐

  1. IoC 思想简单而深邃
  2. ThreadLocal
  3. 终端的颜值担当-WindTerm
  4. Spring 三级缓存
  5. RBAC 权限设计(二)
ConcurrentHashMapJava中的个并发容器,用于在多线程环境中安全地存储和访问键值对。它使用了些特殊的技术来提高其并发性能。 ConcurrentHashMap源码分析可以从几个关键点开始。首先,它使用了大量的CAS(Compare and Swap)操作来代替传统的重量级锁操作,从而提高了并发性能。只有在节点实际变动的过程中才会进行加锁操作,这样可以减少对整个容器的锁竞争。 其次,ConcurrentHashMap的数据结构是由多个Segment组成的,每个Segment又包含多个HashEntry。这样的设计使得在多线程环境下,不同的线程可以同时对不同的Segment进行操作,从而提高了并发性能。每个Segment都相当于个独立的HashMap,有自己的锁来保证线程安全。 在JDK1.7版本中,ConcurrentHashMap采用了分段锁的设计,即每个Segment都有自己的锁。这样的设计可以在多线程环境下提供更好的并发性能,因为不同的线程可以同时对不同的Segment进行读写操作,从而减少了锁竞争。 总的来说,ConcurrentHashMap通过使用CAS操作、分段锁以及特定的数据结构来实现线程安全的并发访问。这使得它成为在多线程环境中高效地存储和访问键值对的选择。123 #### 引用[.reference_title] - *1* [ConcurrentHashMap 源码解析](https://blog.youkuaiyun.com/Vampirelzl/article/details/126548972)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *2* *3* [ConcurrentHashMap源码分析](https://blog.youkuaiyun.com/java123456111/article/details/124883950)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值