ConcurrentHashMap源码解析(每行都有完整解析)

本文详细解析了ConcurrentHashMap的工作原理,包括线程安全的实现机制、与HashMap的区别、链表与红黑树的转换策略,以及在高并发场景下的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

带着问题看文章:
  1. ConcurrentHashMap是怎样实现线程安全的?
  2. 它和HashMap有什么异同呢?
  3. 在什么样的场景下用ConcurrentHashMap呢?
  4. 为什么HashMap和ConcurrentHashMap的链表数目>=8才转换成红黑树?为什么<=6才从树转换成链表?
put()方法入口:

在这里插入图片描述

本质上调用了putVal()方法
public V put(K key, V value) {
        return putVal(key, value, false);
    }
进入putVal()方法:
 /**
     * 
     * @param key 键
     * @param value 值
     * @param onlyIfAbsent 是否不替换原值?put方法默认为false,表示会替换原值
     * @return 返回原值
     */
 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;
            //如果tab为null,初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //如果当前索引位置没有值,直接创建
            //tabAt的作用:final修饰,使用Unsafe类,计算放在槽的下标位置,返回一个Node节点
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //cas 在 i 位置创建新的元素,当 i 位置是空时,即能创建成功,结束for自循,否则继续自旋,casTabAt()里面调用了使用Unsafe类的compareAndSwapObject方法
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin:bin为空时使用了无锁的方式
            }
            //如果当前节点正在扩容,就会辅助扩容
            //正在扩容的节点叫转移节点,它的 hash 值是固定的,都是 MOVED == -1
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //当前槽点有值
            else {
                V oldVal = null;
                //同步代码块,锁住当前节点f是前面定义的Node对象,锁对象,保证了安全(防止在高并发的场景下,多线程更改同一个数据带来的线程安全问题)
                synchronized (f) {
                    //这里再次判断索引i位置的数据有没有被更改
                    if (tabAt(tab, i) == f) {
                        //如果当前节点是链表
                        if (fh >= 0) {
                            //记录链表中元素的个数
                            binCount = 1;
                            //循环遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //如果key已经存在
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    //onlyIfAbsent = false,放入新值,返回旧值
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //如果key不存在
                                //把新增的元素赋值到链表的最后(采用尾插法),然后break循环
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        //如果当前节点是红黑树节点
                        //TreeBin 是内部类,持有红黑树的引用,使用了volatile和Unsafel类的方法(使用CAS+park/unpark机制实现加锁和解锁),保证其操作的线程安全
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            //满足if的话,把老的值给oldVal
                            //在putTreeVal方法里面,在给红黑树重新着色旋转的时候会锁住红黑树的根节点
                            //putTreeVal方法:红黑树的put方法
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                //满足条件,新值替换旧值
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                //如果binCount有值
                if (binCount != 0) {
                    //如果binCount >= 8,就把链表转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    //如果oldVal有值,返回oldVal
                    if (oldVal != null)
                        return oldVal;
                    //break掉这个循环(注意这个循环是最外层的)
                    //这一步几乎走不到,因为槽点已经上锁,只有在红黑树或者链表新增失败的时候
                    //才会走到这里,但是这两者新增都是自旋的(相当于一直重试),几乎不会失败
                    break;
                }
            }
        }
        //检查是否需要扩容,addCount()内部维护了扩容的方案,如果需要扩容,就调用transfer 方法去扩容
        addCount(1L, binCount);
        return null;
    }

put方法逻辑:

  1. 判断Node[]数组是否初始化,没有则进行初始化操作
  2. 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败break掉,进入下一次循环
  3. 检查内部是否在扩容,如果在扩容就帮助其一起扩容
  4. 如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
    4.1 如果是Node(链表结构)则执行链表的添加操作
    4.2 如果是TreeNode(树形结构)则执行树的添加操作
  5. 判断链表长度是否达到临界值8,如果大于8就把链表转换成树结构

hash值得计算方式:

//转化为2进制为32个1
static final int HASH_BITS = 0x7fffffff;
	/**
     * 高16位和低16位异或再和HASH_BITS进行与运算
     * @param h key的hashCode值
     * @return hash值
     */
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

为什么要这么玩?为了增加散列度,使用了“位扰动”的方式,让元素更均匀的分布在有限的槽上
运算符^ &的含义:与、或、异或运算

问题回答:

  1. ConcurrentHashMap是怎样实现线程安全的?

JDK8之前,使用的是分段锁实现的,将整个数组结构分为多段,每段加锁;在JDK8之后,锁粒度更细,取消分段锁,每个bucket都单独加锁(锁住首节点),采用CAS+Synchronized的方式
首先使用无锁操作CAS插入头节点,失败则循环重试
若头节点已存在,则尝试获取头节点的同步锁,再操作

  1. 它和HashMap有什么异同呢?

相同点:
数据结构和设计的方式基本相同,都采用了数组+链表/红黑树的结构
不同点:
ConcurrentHashMap的变量采用的volatile关键字修饰,保证多线程之间的可见性
ConcurrentHashMap添加sizeCtl关键字控制初始化和扩容,负数表示正在初始化或扩容(-1表示正在初始化,-(1+n)表示多少个线程在辅助扩容),0表示没有进行初始化,大于0表示下次扩容/初始化后容量的大小
HashMap可以有null值null键,ConcurrentHashMap不允许(HashTable也不可以),添加会报空指针异常

  1. 在什么样的场景下用ConcurrentHashMap呢?
  2. 为什么HashMap和ConcurrentHashMap的链表数目>=8才转换成红黑树?为什么<=6才从树转换成链表?

当hashCode离散性很好的时候,树型转化用到的概率非常小,因为数据均匀分布在每个桶中,几乎不会有桶中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有桶中节点的分布频率会遵循泊松分布,我们可以看到,一个桶中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是拍拍屁股决定的,而是根据概率统计决定的。

后续方法请看这篇文章:
https://blog.youkuaiyun.com/weixin_44397907/article/details/115699102?spm=1001.2014.3001.5501

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值