关于jdk1.8中ConcurrentHashMap

本文详细介绍了JDK1.8中ConcurrentHashMap的重要概念、数据结构和操作,包括table、TreeBin、TreeNode、ForwardingNode等,并探讨了其扩容实现、并发控制策略,如transfer()方法和helpTransfer()方法,以及并发扩容总结和查找删除操作。通过对构造方法、put()方法、resizeStamp()等的分析,揭示了ConcurrentHashMap在并发环境下的高效性和安全性。
摘要由CSDN通过智能技术生成

1.前言

Java JDK升级到1.8后有些集合类的实现有了变化,
其中ConcurrentHashMap就有进行结构上的大调整。
jdk1.6、1.7实现的我不会想了解去百度吧

2. 重要概念

想要了解ConcurrentHashMap 你至少要对HashMap有一定的了解吧?
HashMap(1.8)和ConcurrentHashMap(1.8) 底层都是hash表+链表+红黑树
如果对红黑树不了解的可以查看https://blog.csdn.net/A980719/article/details/120264812

当你对hashMap有一定的了解后要想读懂ConcurrentHashMap还要知道以下重要概念

2.1 table

所有数据都存在table中,table的容量会根据实际情况进行扩容,
table[i]存放的数据类型有以下3种:

  1. Node 普通结点类型,
  2. 表示链表头结点,
  3. 红黑树根节点。
    在这里插入图片描述

2.2 TreeBin 用于包装红黑树结构的结点类型

这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。

2.3 TreeNode:

树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问

2.4、ForwardingNode:

一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1(当hash为-1表示正在扩容). 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。

3.5 nextTable

扩容时用于存放数据的变量,扩容完成后会置为null。ForwardingNode的nextTable指针在扩容时指向它

3.6 sizeCtl

以volatile修饰的sizeCtl用于数组初始化与扩容控制,它有以下几个值:

当前未初始化:
	= 0  //未指定初始容量
	> 0  //由指定的初始容量计算而来,再找最近的2的幂次方。
		//比如传入6,计算公式为6+6/2+1=10,最近的2的幂次方为16,所以sizeCtl就为16。
初始化中:
	= -1 //table正在初始化
	= -N //N是int类型,分为两部分,高15位是指定容量标识,低16位表示
	     //并行扩容线程数+1,具体在resizeStamp函数介绍。
初始化完成:
	=table.length * 0.75  //扩容阈值调为table容量大小的0.75倍
	

一个思考:这个sizeCtl是volatile的,那么他是线程可见的,它是所有修改都在CAS中进行,
但是sizeCtl为什么不设计成LongAdder(jdk8出现的)类型呢?
或者设计成AtomicLong(在高并发的情况下比LongAdder低效),这样就能减少自己操作CAS了。

3.相关操纵(创建和删除)

3.1 ConcurrentHashMap构造方法

3.1.1 ConcurrentHashMap()
public ConcurrentHashMap() {
   
    }

说明:该构造函数用于创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。

3.1.2 ConcurrentHashMap(int)
    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;
    }

说明:该构造函数用于创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。

3.1.3 ConcurrentHashMap(int,float)
   public ConcurrentHashMap(int initialCapacity, float loadFactor) {
   
        this(initialCapacity, loadFactor, 1);
    }

说明:该构造函数用于创建一个带有指定初始容量、加载因子和默认 concurrencyLevel (1) 的新的空映射。

3.1.4 ConcurrentHashMap(int, float, int)
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
   
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) // 合法性判断
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

说明:该构造函数用于创建一个带有指定初始容量、加载因子和并发级别的新的空映射。

传入的加载因子只对创建表时有影响 并不会改变程序里的加载因子
在这里插入图片描述

对于构造函数而言,会根据输入的initialCapacity的大小来确定一个最小的且大于等于initialCapacity大小的2的n次幂,如initialCapacity为15,则sizeCtl为16,若initialCapacity为16,则sizeCtl为16。若initialCapacity大小超过了允许的最大值,则sizeCtl为最大值。值得注意的是,构造函数中的concurrencyLevel参数已经在JDK1.8中的意义发生了很大的变化,其并不代表所允许的并发数,其只是用来确定sizeCtl大小,在JDK1.8中的并发控制都是针对具体的桶而言,即有多少个桶就可以允许多少个并发数。
   在这里插入图片描述

3.2 put()方法

   ConcurrentHashMap最常用的就是put和get两个方法。现在来介绍put方法,这个put方法依然沿用HashMap的put方法的思想,根据hash值计算这个新插入的点在table中的位置i,如果i位置是空的,直接放进去,否则进行判断,如果i位置是树节点,按照树的方式插入新的节点,否则把i插入到链表的末尾。ConcurrentHashMap中依然沿用这个思想,有一个最重要的不同点就是ConcurrentHashMap不允许key或value为null值。另外由于涉及到多线程,put方法就要复杂一点。在多线程中可能有以下两个情况。

  1. 如果一个或多个线程正在对ConcurrentHashMap进行扩容操作,当前线程也要进入扩容的操作中。这个扩容的操作之所以能被检测到,是因为transfer方法中在空结点上插入forward节点(hash值为-1),如果检测到需要插入的位置被forward节点占有,就帮助进行扩容;
  2. 如果检测到要插入的节点是非空且不是forward节点,就对这个节点加锁,这样就保证了线程安全。尽管这个有一些影响效率,但是还是会比hashTable的synchronized要好得多。

整体流程就是首先定义不允许key或value为null的情况放入,对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置。

  1. 如果这个位置是空的,那么直接放入,而且不需要加锁操作。

  2. 如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。否则依次向后遍历,直到链表尾插入这个结点。 如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。

    final V putVal(K key, V value, boolean onlyIfAbsent) {
   
    //这里也就定了key和Value不能为空
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;// I处结点数量,主要用于每次加入结点后查看是否要由链表转为红黑树
        for (Node<K, V>[] tab = table; ; ) {
    //CAS经典写法,不成功无限重试,让再次进行循环进行相应操作。
            Node<K, V> f;
            int n, i, fh;
            // f 当前hash的头节点 n 表的长度 i 当前位置 fh 当前hash的头节点 的hash值
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();//只进行初始化和hashMap的初始化不同 
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
   
                if (casTabAt(tab, i, null,
                        new Node<K, V>(hash, key, value, null))) //如果为null创建Node对象做为链表首结点
                    break;                   // no lock when adding to empty bin
            } else if ((fh = f.hash) == MOVED)//MOVED==-1  如果添加时hash表正在扩容 反正闲着也是闲着  帮忙去扩容吧
                tab = helpTransfer(tab, f);
            else {
   
                V oldVal = null;
                synchronized (f) {
   //说明当前节点已经有元素 上锁在当前节点的末尾插入新节点
                    if (tabAt(tab, i) == f) {
   //双重检查i处结点未变化     猜测 上面的操作时当前结点的同节点被删除 导致不相同
                        if (fh >= 0) {
   //表明是链表结点类型,hash值是大于0的,即spread()方法计算而来
                            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)))) {
   //放存在相同的key
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)//判断是否需要修改值
                                        e.val = value;
                                    break;
                                }
                                Node<K, V> pred = e;
                                if ((e = e.next) == null) {
   //当不存在相同的key 时在结尾插入
                                    pred.next = new Node<K, V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        } else if (f instanceof TreeBin) {
   //f.hash== -2
                            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) {
   
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);//转换成红黑树 
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//判断是否扩容
        return null;
    }

3.3 spread()

jdk1.8的hash策略,与以往版本一样都是为了减少hash冲突:
和hashMap的hash()方法基本一样只不过key不能为空;

	
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一叶一菩提魁

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

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

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

打赏作者

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

抵扣说明:

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

余额充值