秋招准备之——深入理解ConcurrentHashMap(JDK1.8)

秋招复习笔记系列目录(不断更新中):

一、前言

以前感觉HashMap难懂,直到我看了ConcurrentHashMap。。。不过,等真的读懂了源码,不得不感叹,Doug Lea大爷还是你大爷,看的过程中,不时惊呼:原来是这样啊!这也太牛了!好了,首先介绍一下,ConcurrentHashMap是一个线程安全的HashMap,其主要采用CAS操作+synchronized锁的方式,实现线程安全,其中synchronize锁的粒度为桶中头结点(包括链表Node结点,包装红黑树的TreeBin结点),底层依然由 “数组”+链表+红黑树 的方式实现。

二、基础

2.1 几个重要的属性

  • sizeCtl: 它是一个控制标志符,取值不同有不同的含义:

    • 负数代表正在进行初始化或扩容操作
    • -1代表正在初始化
    • -N 表示有N-1个线程正在进行扩容操作
    • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。
  • 桶中元素的hash值的含义:

    • ①hash==-1 (MOVED) : 表示当前节点是ForwardingNode节点
    • ②hash==-2 (TREEBIN) : 表示当前节点已经树化,且当前节点为TreeBin对象,TreeBin节点代理红黑树操作
    • ③hash==-3 (RESERVED) : 临时保留的哈希

2.2 几个重要的内部类

  • Node节点类:HashMap中的节点类似,只是其val变量和next指针都用volatile来修饰。且不允许调用setValue方法修改Node的value值。这个类是后面三个类的基类
  • TreeNode: 树节点类,当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNodeConcurrentHashMap继承自Node类,而并非HashMap中的继承自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。
  • TreeBin: 这个类并不负责包装用户的key、value信息,而是直接放了一个名为rootTreeNode节点,这个是他所包装的红黑树的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。HashMap桶中存储的是TreeNode结点,这里的根本原因是==并发过程中,有可能因为红黑树的调整,树的形状会发生变化,这样的话,桶中的第一个元素就变了,而使用TreeBin包装的话,就不会出现这种情况。 这种类型的节点, hash值为-2,从下面的构造函数中就可以看出来。这样我们通过hash值是否等于-2就可以判断桶中的节点是否是红黑树。
    在这里插入图片描述
  • ForwardingNode: 一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。在扩容操作中,我们需要对每个桶中的结点进行分离和转移,如果某个桶结点中所有节点都已经迁移完成了(已经被转移到新表 nextTable 中了),那么会在原 table 表的该位置挂上一个 ForwardingNode 结点,说明此桶已经完成迁移。这种类型的节点的hash值是-1,通过构造函数也可以看出来
    在这里插入图片描述

2.3 使用CAS操作的三个核心方法

类中定义了三个静态的用于CAS操作的方法:

 	//获得在i位置上的Node节点
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
   
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
		//利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
   
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
		//利用volatile方法设置节点位置的值
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
   
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

三、数组的初始化

数组的初始化是在ConcurrentHashMap插入元素的时候发生的,如调用put等方法时发生的。初始化操作在initTable方法中,该没有加锁,因为采取的策略是,当sizeCtl<0时,说明已经有线程在给扩容了,这个线程就会调用Thread.yield()让出一次CPU执行时间。看代码,我们可以看到上面说的sizeCtl的作用:负数表示正在扩容,扩容完成后,用来表示阈值。

private final Node<K,V>[] initTable() {
   
    Node<K,V>[] tab; int sc;
    //tab为空时才进行初始化
    while ((tab = table) == null || tab.length == 0) {
   
        //如果sizeCtl<0,说明有其他的线程正在初始化,当前线程让出资源
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
   //当前没有线程扩容,那就利用CAS方法把sizectl的值置为-1 表示本线程正在进行初始化
            try {
   
                if ((tab = table) == null || tab.length == 0) {
   
                    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);//相当于0.75*n 用sizeCtl来表示阈值
                }
            } finally {
   
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

四、put过程

4.1 整体过程

整个put过程,主要在putVal函数中实现,具体过程为:

  • ① 如果数组未没初始化,则先去初始化
  • ② 如果对应的桶中的元素为空,那就新建一个链表节点,然后利用CAS操作将其放到桶中的位置。这个过程是在③前面的,我们知道,扩容过程中,每个桶位置迁移节点结束后,会将这个节点设置为ForwardingNode,所以这种情况下,你尽管放,放了以后,扩容的线程总会遍历到这个节点,然后将这个节点迁移到新数组中。
  • ③ 如果有线程在扩容,那就先去帮助扩容,扩容结束后,再重新put
  • ④ 最后,如果当前桶中已经有元素了,那就用synchronized锁住当前桶中的节点,然后在桶中插入元素,插入的时候,要么插入到链表中,要么插入到红黑树中。我们发现,这里的锁粒度是很小的,就锁住一个桶,不像JDK1.7中的ConcurrentHashMap,是分段锁,锁住很多的桶,所以并发效率更高。
  • ⑤ 插入结束后,如果是插入到链表中,那去看看链表的长度有没有超过长度阈值8,如果超过了,就要将链表转换成红黑树。
  • ⑥ 最后,让HashMap的size加一(这里其实是用baseCount来记录长度的,而且处理的时候很复杂,继续看下面)。

整个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 (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
   
        ConcurrentHashMap.Node<K,V> f; int n, i, fh;
        //哈希表未初始化,则先对数组进行初始化操作,见上面的初始化操作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //如果桶中元素为空,则直接通过CAS操作放到对应位置
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
   
            if (casTabAt(tab, i, null,
                    new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
                break;                    // no lock when adding to empty bin
        }
        //如果有线程正在扩容,则两个线程一起帮忙扩容,扩容完毕后tab指向新table
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
   
            V oldVal = null;
            synchronized (f) {
   //持有桶中的第一个节点的锁
                if (tabAt(tab, i) == f) {
   //加锁后再去判断,桶中的结点有没有变化
                    if (fh >= 0) {
   //表明是链表结点类型
                        binCount = 1;
                        for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
   
                            K ek;
                            if (e.hash == hash &&//找到对象,将其val替换
                                    ((ek = e.key) == key ||
                                            (ek != null && key.equals(ek)))) {
   
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            ConcurrentHashMap.Node<K,V> pred = e;
                            if ((e = e.next) == null) {
   //没找到,插入到尾部
                                pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
                                        value, null);
                                break;
                            }
                        }
                    }
                    //红黑树节点的插入
                    else if (f instanceof ConcurrentHashMap.TreeBin) {
   
                        ConcurrentHashMap.Node<K,V> p;
                        binCount = 2;
                        if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
                                value)) != null) {
   
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            //binCount不为0说明插入了新节点,为0说明在空桶中插入了一个节点(这种情况不需要树化)
            if (binCount != 0) {
   
                //默认桶中结点数超过8个数据结构会转为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

MeteorChenBo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值