JDK8中ConcurrentHashMap底层源码解析-put和putVal方法以及数组的初始化


前言

JDK1.8 中的ConcurrentHashMap选择了与 HashMap相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。本篇文章以JDK8为例,对ConcurrentHashMap的底层源码做一个解析,帮助大家更好的理解ConcurrentHashMap的底层原理。


一、容器初始化

我们在创建ConcurrentHashMap时,通常有以下两种做法,这2个构造方法中都没有对内部的数组做初始化

public class Test {
    public static void main(String[] args) {
       //1.无参构造
        ConcurrentHashMap concurrentHashMap=new ConcurrentHashMap();
       //2.传递进来一个初始容量
        ConcurrentHashMap concurrentHashMap2=new ConcurrentHashMap(32);
    }
}

无参构造中没有维护任何变量的操作,如果调用该方法,数组长度默认是16

public ConcurrentHashMap() {
}

如果传递进来一个初始容量,ConcurrentHashMap会基于这个值计算一个比这个值大的2的幂次方数作为初始容量

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

注意:调用这个方法,得到的初始容量和我们之前讲的HashMap不同,即使你传递的是一个2的幂次方数,该方法计算出来的初始容量依然是比这个值大的2的幂次方数,而HashMap是大于或者等于。

同时,在上面的构造方法中涉及到了一个变量sizeCtl,这个变量是一个非常重要的变量,而且具有非常丰富的含义,它的值不同,对应的含义也不一样,这里我们先对这个变量不同的值的含义做一下说明,后续源码分析过程中,进一步解释。

  1. sizeCtl为0,代表数组未初始化, 且数组的初始容量为16
  2. sizeCtl为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值
  3. sizeCtl为-1,表示数组正在进行初始化
  4. sizeCtl小于0,并且不是-1,表示数组正在扩容, -(1+n),表示此时有n个线程正在共同完成数组的扩容操作

二、put和putVal方法

当我们调用put方法往里面添加一个元素时,在put方法中会调用putVal方法

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

由于JDK8中ConcurrentHashMap的putVal()方法太长,在这里我们可以先思考一下这个方法要做什么:

  1. 如果数组还未初始化,先对数组进行初始化
  2. 基于key计算hash值,并进行一定的扰动,同时需要计算一个数组下标,代表要存放的位置
  3. 把key,value,hash封装到一个Node中,利用cas将元素添加对应的位置中去

先知道我们要做什么,在去看源码,就不会那么难懂了,接下来看putVal()的底层源码,注意第一次看可能会有点懵,但是多看几遍一定会有收获。

final V putVal(K key, V value, boolean onlyIfAbsent) {
        //如果有空值或者空键,直接抛异常,而HashMap允许添加一个null键值对
        if (key == null || value == null) throw new NullPointerException();
        //基于key计算hash值,并进行一定的扰动
        int hash = spread(key.hashCode());
        //记录某个位置上元素的个数,如果超过8个,会转成红黑树
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果数组还未初始化,先对数组进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //计算数组下标,如果该位置上没有元素,则利用cas将元素添加,添加成功返回true
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //cas+自旋(和外侧的for构成自旋循环),保证元素添加安全
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;         // no lock when adding to empty bin   如果是该位置的第一个元素,直接用 CAS原则插入,无需加锁
            }
            //如果数组下标元素的hash值为MOVED,证明正在扩容,那么协助扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            //如果数组下标元素不为空,且当前没有处于扩容操作,进行元素添加
            else {
                V oldVal = null;
                 //对第一个结点进行加锁,保证线程安全,执行元素添加操作,添加期间判断是否有重复键
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                     //普通链表节点,遍历链表(同时检查有无重复键,并且用binCount记录链表长度),将元素加到链表末尾中去
                        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;
                                }
                            }
                        }
                        //树节点,将元素添加到红黑树中,也要检查有无重复键
                        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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //链表长度大于/等于8,将链表转成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    //如果是重复键,直接将旧值返回
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //不存在重复键,添加的是新元素,要维护集合长度,并判断是否要进行扩容操作
        addCount(1L, binCount);
        return null;
    }

通过以上源码,我们可以看到,当需要添加元素时,会针对当前元素所对应的桶位进行加锁操作,这样一方面保证元素添加时,多线程的安全,同时对某个桶位加锁不会影响其他桶位的操作,进一步提升多线程的并发效率。
如果不清楚CAS的可以看这篇文章辅助学习:CAS操作原理与实现

下面用一张图演示ConcurrentHashMap插入元素时的场景。
在这里插入图片描述


三、initTable方法

在putVal方法中,如果数组还未初始化,要先对数组进行初始化,调用的是initTable(),现在我们来看一下这个方法的源码:

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //cas+自旋,保证线程安全,对数组进行初始化操作
    while ((tab = table) == null || tab.length == 0) {
        //如果sizeCtl的值(-1)小于0,说明此时正在初始化, 让出cpu
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin  线程让步
        //cas修改sizeCtl的值为-1,修改成功,进行数组初始化,失败,继续自旋
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    //sizeCtl为0,取默认长度16,否则取sizeCtl的值
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    //基于初始长度,构建数组对象
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //计算扩容阈值,并赋值给sc
                    sc = n - (n >>> 2);
                }
            } finally {
                //将扩容阈值,赋值给sizeCtl
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

小结:数组初始化的时候利用了cas+自旋,保证线程安全,这里用到了一个上文中提到的非常重要的变量sizeCtl变量,利用cas修改sizeCtl的值为-1(sizeCtl为-1,表示数组正在进行初始化),基于初始长度,构建数组对象并计算扩容阈值赋值给sizeCtl(如果数组已经初始化,那么其记录的是数组的扩容阈值)。


总结

个人认为JDK8中ConcurrentHashMap的put和putVal方法源码跟HashMap存在很多相似的地方,不同的是ConcurrentHashMap利用了一些CAS+自旋的思想,如果该位置上没有元素直接用 CAS原则插入,无需加锁,否则就第一个结点进行加锁,保证线程安全。


VMware中,使用NAT(Network Address Translation)网络配置可以实现虚拟机访问外网,并且允许物理机访问VMware中的虚拟机。以下是一些步骤和注意事项来配置VMware NAT网络: 1. 首先,在物理机上打开VMware择你的虚拟机。 2. 在VMware的菜单栏中,择"编辑" > "虚拟网络编辑器"。 3. 在虚拟网络编辑器中,择"NAT模式"并确保择了NAT模式项。检查所的NAT模式的网络段(如192.168.136.xxx),确保与物理机的网络段相同,并记录下网关(如192.168.136.2)。 4. 在虚拟机中,打开命令行终端并执行以下命令:cd /etc/sysconfig/network-scripts 5. 编辑网络配置文件ifcfg-ens33(具体文件名可能因系统版本而异)。可以使用文本编辑器(如vi)打开该文件。 6. 在文件中,设置以下参数: - BOOTPROTO设置为"static",表示使用静态IP地址。 - IPADDR设置为虚拟机的IP地址,例如192.168.136.xxx。 - NETMASK设置为子网掩码,通常为255.255.255.0。 - GATEWAY设置为网关IP地址,例如192.168.136.2(与NAT模式的网关相同)。 7. 保存并关闭文件。 8. 重新启动虚拟机的网络服务,可以使用以下命令:sudo systemctl restart network(具体命令可能因系统版本而异)。 通过以上步骤,你可以配置VMware的NAT网络使虚拟机可以访问外网,并允许物理机访问虚拟机。请记得在配置过程中注意取消勾使用本地DHCP服务将IP地址分配给虚拟机项,以避免网络拥堵和延迟问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [VMware NAT网络设置](https://blog.csdn.net/hgc2020/article/details/130296995)[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_1"}}] [.reference_item style="max-width: 50%"] - *3* [VMware 虚拟机设置nat网络](https://blog.csdn.net/weixin_42803027/article/details/124595457)[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_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JinziH Never Give Up

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值