16-ConcurrentHashMap 源码解析和设计思路(并发集合)

注:源码系列文章主要是对某付费专栏的总结记录。如有侵权,请联系删除。

1 结构

ConcurrentHashMap 的底层数据结构和方法的实现细节和 HashMap 大体一致,但两者在类结构上却没有任何关联,如下类图:

ConcurrentHashMap 结构图
看 ConcurrentHashMap 的源码,我们会发现很多方法和代码和 HashMap 很相似,那可能会产生疑问,为什么不继承 HashMap 呢?继承的确是个好办法,但是 ConcurrentHashMap 都是在方法中间进行一些加锁操作,也就是说加锁把方法切割了,继承就很难解决这个问题。

ConcurrentHashMap 和 HashMap 两者的相似之处:

  1. 数组、链表结构几乎相同,所以底层对数据结构的操作思路是相同的(只是思路相同,底层实现不同,因为 ConcurrentHashMap 需要在方法实现中间加锁);
  2. 都实现了 Map 接口,继承了AbstractMap 抽象类,所以大多数的方法也都是相同的,HashMap 有的方法,ConcurrentHashMap 几乎都有,所以我们需要从 HashMap 切换到 ConcurrentHashMap 时,无需关心两者之间的兼容问题。

不同之处:

  1. 红黑树结构略有不同,HashMap 的红黑树中的节点叫做 TreeNode,TreeNode 不仅仅有属性,还维护着红黑树的结构,比如查找(TreeNode.getTreeNode),新增(TreeNode.putTreeVal)等;ConcurrentHashMap 中红黑树被拆分成两块,TreeNode 仅仅维护的属性和查找功能,新增了 TreeBin,来维护红黑树结构,并负责根节点的加锁和解锁(TreeBin.putTreeVal 负责新增,并有 lockRootunlockRoot)。
  2. 新增 ForwardingNode(转移)节点,扩容的时候会使用到,通过使用该节点,来保证扩容时的线程安全。

2 put

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 计算 hash
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 通过自旋死循环保证一定可以新增成功
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // table 为空进行初始化操作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 如果当前桶位置没有值,直接创建
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        	// CAS 在 i 位置创建新的元素,当 i 位置是 null 时才能创建成功,结束 for 自循环,否则继续自旋
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 转移节点的 hash 值是固定的,都是 MOVED
        // 如果当前桶节点是转移节点,表示该槽点正在扩容,就会一直等待扩容完成
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 桶位置有值
        else {
            V oldVal = null;
            // 锁定当前桶,其余线程不能操作,保证了安全
            synchronized (f) {
            	// 这里再次判断索引 i 位置的数据没有被修改
                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;
                            }
                        }
                    }
                    // 红黑树
                    // 这里没有使用 TreeNode,使用的是 TreeBin,TreeNode 只是红黑树的一个节点
                    // TreeBin 持有红黑树的引用,并且会对其加锁,保证其操作的线程安全
                    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;
                        }
                    }
                }
            }
            // binCount 不为空表示当前索引 i 位置有值
            if (binCount != 0) {
            	// 链表是否转换为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // oldVal 存在则直接返回,也就是不需要走后面的检查扩容步骤
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 检查容器是否需要扩容,如果需要扩容,调用 transfer 方法去扩容
    // 如果已经在扩容中了,则检查扩容是否完成
    addCount(1L, binCount);
    return null;
}

下面终点说下 ConcurrentHashMap 在 put 过程中,采用了哪些手段来保证线程安全。

2.1 数组初始化时的线程安全 initTable

数组初始化时,首先通过自旋来保证一定可以初始化成功,然后通过

// 初始化数组,通过对 sizeCtl 的变量赋值来保证数组只被初始化一次
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        // 通过自旋保证初始化成功
        while ((tab = table) == null || tab.length == 0) {
        	// 如果 sizeCtl 小于0(也就是等于 -1) 时表示有线程正在初始化,释放当前 CPU 的调度权,重新发起锁的竞争
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            // CAS 赋值保证了当前只有一个线程初始化 SIZECTL 为 -1,保证了数组的初始化的安全性
            // -1: 待更新的值
            // sc: 期望值
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                	// 有可能执行到这里的时候,table 已经不为空了,类似于单例模式的双重 check
                    if ((tab = table) == null || tab.length == 0) {
                    	// 初始化数组大小为 16 (DEFAULT_CAPACITY)
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

2.2 新增节点值时的线程安全

此时为了保证线程安全,做了四处优化:

  1. 通过自旋死循环保证一定可以新增成功。

在新增之前,通过 for (Node<K,V>[] tab = table;;) 这样的死循环来保证新增一定可以成功,一旦新增成功,就可以退出当前死循环,新增失败的话,会重复新增的步骤,直到新增成功为止。

  1. 当前节点为空时,通过 CAS 新增

Java 这里的写法非常严谨,没有在判断节点为空的情况下直接赋值,因为在判断节点为空和赋值的瞬间,很有可能节点已经被其它线程赋值了,所以我们采用 CAS 算法,能够保证节点为空的情况下赋值成功,如果恰好节点已经被其它线程赋值,当前 CAS 操作失败,会再次执行 for 自旋,再走节点有值的 put 流程,这里就是自旋 + CAS 的组合。

  1. 当前节点有值,锁住当前节点。

新增时,如果当前节点有值,就是 key 的 hash 冲突的情况,此时节点上可能是链表或红黑树,我们通过锁住节点,来保证同一时刻只会有一个线程对节点进行修改:synchronized (f)

  1. 红黑树旋转时,锁住红黑树的根节点,保证同一时刻,当前红黑树只能被一个线程旋转。

Snipaste_2020-04-20_11-02-43.png

在旋转之前,锁住红黑树的根节点。如果锁定失败,将一直竞争,直到得到为止。

通过以上四点,保证了各种情况下的新增(不考虑扩容的情况下),都是些线程安全的,通过自旋 + CAS + 锁,实现的很巧妙,值得我们借鉴。

2.3 扩容时的线程安全

略(暂未理解)

在这里插入图片描述

3 get

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    // 数组不为空并且当前槽点的数据不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 槽点的第一个值相等则直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 如果是红黑树或者转移节点,使用其对应的 find 方法
        // 扩容时会将槽点设置为转移节点,转移节点的 hash 为 MOVED(-1)
        // 红黑树: ???
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 如果是链表则遍历查找
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

------------------------------------- END -------------------------------------

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMap 是一个并发安全的哈希表实现,它由多个段(Segment)组成,每个段包含一个 HashEntry[] 数组(在 JDK 1.7 )或 Node<K, V>[] 数组(在 JDK 1.8 )。每个段都相当于一个小的哈希表,用于存储一部分键值对。这种分段的设计可以提供更高的并发性能。 在 JDK 1.7 ConcurrentHashMap 使用分段锁机制来保证线程安全性。每个段都有自己的锁,不同的线程可以同时访问不同的段,从而提高了并发性能。 而在 JDK 1.8 ConcurrentHashMap 使用了更加高效的 CAS (Compare and Swap) 操作来实现线程安全性,取代了分段锁机制。这样,在并发操作时,不同的线程可以在没有明显的互斥操作的情况下同时进行读写操作,提高了并发性能。 所以,如果在代码遇到了 "- ConcurrentHashMap 无法解析为类型" 的错误,可能是由于没有正确导入 ConcurrentHashMap 类的原因。请确保你的代码已经正确导入了对应的类,并且编译环境包含了对应的 JDK 版本。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [搞定HashMap线程不安全问题-----ConcurrentHashMap源码解析](https://blog.csdn.net/ZMXQQ233/article/details/108446246)[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^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [高并发(六)--ConcurrentHashMap 源码解析](https://blog.csdn.net/m0_52675592/article/details/116070659)[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^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值