Java并发进阶系列:深度讨论官方关于jdk1.8ConcurrentHashMap的computeIfAbsent源代码修复逻辑

在文章中《深度解析官方关于jdk1.8的resizeStamp的bug处理过程》,我们讨论关于CHM的核心设计——resizeStam需要修复的处理过程,本文再次基于openJDK的bugs讨论组提出的CHM源代码另外一个会造成死循环的bug,默认读者已经掌握CHM的核心源代码实现,否则无法从本文的讨论中获益。文章前部分先把computeIfAbsent的bug成因分析清楚,再来介绍官网ConcurrentHashMap.computeIfAbsent stuck in an endless loop的讨论过程,这样更容易看懂相关内容。

研究openJDK官方公布的相关源码bug有何“收益”:

虽然这些bug不是特别严重,修复起来也即几行代码,但如果想要解决这种看似“简单的bug”,要求对CHM设计原理、类、方法实现细节足够熟悉,也就是说,你要具备(至少在这个bug上下文的类、方法范围内)和源代码设计者同等思考视角才能去挖掘bug的本质原因并提出合理的修复建议。换句话说,你研究的不是这个bug本身,而是深入精通整个类的源代码实现,这种高级收益在日常业务开发几乎无法获得。

认识computeIfAbsent用法

理解computeIfAbsent在一些场合下的用法,有助于帮助切入源代码分析。

computeIfAbsent使用场景1:
package concurrent.demo;
import java.util.concurrent.ConcurrentHashMap;

public class Demo1 {
   
   
    static int computeKeyLength(String key){
   
    // 计算key的长度,将其作为该key对应的value
        return key.length();
    }
    public static void  main(String[] args){
   
   
        ConcurrentHashMap<String,Integer> map=new ConcurrentHashMap<>();
        map.put("foo",1);
        map.computeIfAbsent("foobar",key->computeKeyLength(key));
        System.out.println(map); //输出 {foobar=6, foo=1}
    }

computeIfAbsent字面意思:如果key不在map里面,那么就使用给定的匿名函数(也叫映射函数)将key对应的value“计算出来”。(匿名函数也即lambda语法是jdk1.8语法新特性,这一点不必多说)

按这个思路可以有以下解释:

因为字符串"foobar"这个key不在map里面,因此把它放入map的同时,对应的value要用给定的函数computeKeyLength计算出来,例如这里调用computeKeyLength计算值为6,因此有key=foobar,value=6,将其放入map中。

        map.put("bar",10);
        map.computeIfAbsent("bar",key->computeKeyLength(key));
        System.out.println(map); // 输出:{bar=10}

若key已经在map时,value不会被computeKeyLength(key)的计算值6所覆盖。

当然此demo做了一个不优雅的示范:既然可用匿名函数的写法去写逻辑,就没必要基于方法computeKeyLength去封装多一层,最简便写法如下:

        map.computeIfAbsent("foobar",key->key.length());

注意这个key可以作为匿名函数的入参去参与到计算value,也可以不作为匿名函数的入参,如下:

        map.computeIfAbsent("foobar",key->10);

显然foobar=10。

computeIfAbsent使用场景2:

并发场景下的频率统计:该demo方法其实在并发计数器LongAdder这个类的源码注释里面,Doug Lea已经告诉我们一个经典的场景恰好需要使用computeIfAbsent方法

LongAdders can be used with a java.util.concurrent.ConcurrentHashMap to maintain a scalable frequency map (a form of histogram or multiset). For example, to add a count to a ConcurrentHashMap<String,LongAdder> freqs, initializing if not already present, you can use freqs.computeIfAbsent(k -> new LongAdder()).increment();

package concurrent.demo;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

public class Demo2 {
   
   
    public static void  main(String[] args){
   
   
        ConcurrentHashMap<String,LongAdder> map=new ConcurrentHashMap<>();
        String[] strings={
   
   "foo","bar","foo","foo"};
        for (String key : strings) {
   
   
           map.computeIfAbsent(key,k-> new LongAdder()).increment();
        }
        System.out.println(map); //输出 {bar=1, foo=3}

    }
}

该demo虽然只是用单个线程去执行computeIfAbsent,但逻辑是清晰的:实现对字符串出现次数进行统计

  • 关于LongAdder的分析,它内部其实有一个像ConcurrentHashMap的fullAddCount并发计数逻辑,这里不再讨论,有关研究可参考本博客的文章《Java并发进阶系列:LongAdder高并发计数性能分析》
computeIfAbsent方法源码解析

这部内容要求读者已经掌握jdk1.8的ConcurrentHashMap设计及其关键方法的源代码实现逻辑,否则将难以理解其含义。本节所提的computeIfAbsent是未修复前的版本,这里并不会详细解析computeIfAbsent每一行代码,因为它跟putVal方法逻辑几乎一样,而不同地方可参考以下数字标记的说明:

    public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
   
   
        if (key == null || mappingFunction == null)
            throw new NullPointerException()
        int h = spread(key.hashCode()); 
        V val = null;
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
   
    //看到这个写法应该很熟悉了:自旋+cas机制,为啥要自旋,因为线程不保证自己一次cas就成功,如果和其他线程竞争失败,则需要重试cas,这就是“自旋+cas机制”的黄金搭配。
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
   
    
              	//① 如果key对应的桶位为空,先创建一个保留节点用于接下里的占位逻辑
                Node<K,V> r = new ReservationNode<K,V>();
              	// ②当前线程用保留节点占位当然需要借用独占锁对r对象进行加锁
                synchronized (r) {
   
   
                  	// 在当前桶位放置保留节点用于占位,占位之后就可以给该桶位放入新建的node节点
                    if (casTabAt(tab, i, null, r)) {
   
   
                        binCount = 1;
                        Node<K,V> node = null;
                        try {
   
   
       /*putVal在桶位为空时的逻辑,可看到非常简单,直接使用cas给当前桶位设置新节点,value是给定的value,不需要通过函数计算出value
      if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) {
     				 break;  
      			}
       */
                          // 对于computeIfAbsent,value是需要用给定的匿名函数计算出的,正如前面场景1的“bar”这个key对应的“value”就是使用computeKeyLength(key)计算处理的值
                            if ((val = mappingFunction.apply(key)) != null)
                                node = new Node<K,V>(h, key, val, null);
                        } finally {
   
   
                          	//虽然在②步骤那里已经在桶位i放置了一个ReservationNode用于占位,到了这个步骤才是真正把数据节点node放入桶位i当中
                            setTabAt(tab, i, node);
                        }
                    }
                }
              	// 显然②步骤一定能成功在桶位i放入node节点(binCount=1),既然已经将key和value放入map,那么任务完成,当前线程退出自旋
                if (binCount != 0)
                    break;
            }
            //③如果key定位到的桶位i恰好是一个ForwardingNode占位节点,那么当前线程要去参与“帮助扩容”的逻辑,这里跟putval一样。
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
   
   
               //代码若执行到这,说明桶位i是一个链表或者一棵红色树 
                boolean added = false;
                // 当前线程先给头节点加独占锁,保证当前线程写入节点操作时的独占性
                synchronized (f) {
   
   
          //并发环境,这里当然还要二次检查头节点是不是刚刚加锁前的头节点(也即检查加锁前后的头节点有无被改动过)
                    if (tabAt(tab, i) == f) {
   
   
                         // ④f节点是链表的情况
                        if (fh >= 0) {
   
   
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
   
   
                                K ek; V ev;
                                // 在链表中遇到相同的key,那么就不做更新value操作,返回旧value
                                if (e.hash == h &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
   
   
                                    val = e.val;
                                    break;
                                }
                                Node<K,V> pred = e;
                              	// 尾插法:来到链表尾部
                                if ((e = e.next) == 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值