ConcurrentHashMap源码分析,轻取面试Offer(一)

ConcurrentHashMap

这里主要分析的 jdk1.8中的ConcurrentHashMap,他是java之父Doug Lea之作,很多优秀的开源框架如tomcat、spring、中都大量用到了该类。

【基础好的同学往下走~】====================

首先,看jdk的源码不仅需要了解map是什么,为了更好的理解它到底怎么做到的无锁化还是线程安全的,最好对以下有所了解

  1. 了解 他的数据结构:哈希表(散列表),知道它的原理和为什么。
  2. 线程的概念,生命周期、多线程、什么是线程不安全,为什么会有线程不安全,如何解决。
  3. 了解操作系统,cpu、内存、java代码如何转成字节码转成汇编转成二进制机器指令的,如何操作内存的。
  4. synchronize 关键字,java锁机制。
  5. volatie关键字,内存模型,storeload?
  6. DCL?为什么双重锁验证?单例模式中的dcl。
  7. CAS原理,如何保证?有什么缺陷?(最好顺便了解一下AQS)
  8. ---------------------------------------------------以上大多为理论,最好知晓
  9. java的锁?
  10. HashMap 的 API,大概源码和思路?
  11. ConcurrentHashMap API?

有基础的,没看过源码的直接看这里

看源码的思路?---------

了解他的API

了解数据结构和基本原理

(如果是大型框架:了解他的大概模型

寻找切入点

(这里抛砖引玉,可以举一反三:初始化 , 经常用的api ,结束点等)

比如ConcurrentHashMap经常用到的API:从new,put,get方法开始看

基础好的直接进入正题-----------------------:

先来几个最简单的面试结论热身(前提jdk1.8):

  • ConcurrentHashMap底层数据结构是哈希表(数组 + 链表 + 红黑树)
  • put的返回值
  • 默认大小为16
  • 每次扩容大小为当前的两倍(table[ ] 的长度永远为2的n次幂)
  • 默认扩容阈值为0.75
  • 存放100个key,value对 不指定阈值时,初始大小为256
  • Map<String, String> map = new ConcurrentHashMap<String, String>(100);之后调用 map.size的值为0
  • 使用自定义key类型,需要实现hashcode() 和 equals() 方法
  • 链表长度>8时链表有可能转为红黑树
  • 链表加入元素为尾插法
  • 红黑树结点 < 6时转为链表
  • 整个API几乎没什么锁
  • 扩容时,可多个线程一起扩容
  • 扩容时每个线程获取的任务量和CPU有关
  • sizeCtl 含义很多
  • .....  

知道它是什么,就要问一下为什么,对照上面几个简单结论反问一下自己为什么选用这个,为什么这么做(热身):

  • 哈希表的查找效率比链表、数组、树快很多,时间复杂度为O(1)
  • 如果put的key相同,则返回 oldValue 旧的value值,否则为null,使hashmap使用更灵活
  • 16是研究结论
  • 数组大小永远为2的n次幂是为了后面快速的hash计算,用位运算代替 取余,详细参考下文内容
  • 0.75是参考值,可修改,0.75的计算为(当前大小 - 当前大小无符号右移2位)也比较快
  • 长度始始终为2的n次幂,所以需要至少128的大小,未指定阈值,采用默认值0.75,100/128 > 0.75 会触发扩容,因此为256
  • 初始化在第一次put,无论你是否指定他大小,优点参考懒加载的优点
  • 在第一次hash时候,调用hashcode 方法确定位置,hashcode 相同时会调用equals() 进行比较,因此需要实现
  • 链表长度>8,且table长度 > 64 时才转化为红黑树,否则触发扩容,研究结论,8:参考泊松分布,8的概率<千万分之一
  • 尾插法不如头插法快,但头插法可能造成死循环,同时为了兼容红黑树,因此尾插法更合适
  • < 6 而不是 7 是为了有弹性,避免频繁发生 链表 ---- 红黑树的转换
  • 重量锁会严重降低并发性能,采用CAS来替代锁
  • 有一个线程触发扩容,其他使用ConcurrentHashMap线程不会袖手旁观,会进来帮助一起扩容,每个线程领取一份扩容任务,大家一起做,扩容会更快。
  • 充分压榨CPU的性能,杜绝性能浪费
  • 1.减少内存占用      2.体现写代码人技能巧夺天工

应届生知道以上几个结论一般简单面试够了,但若想碾压面试官,还得继续往下看--

面试题是不断变化的,想要兵来将挡水来土掩就得看源码,看他到底是如何做的。

下面以put为例,简单分析:

put可以放键值对、也可以放map,这里只看最简单的键值对

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

发现put调用了putVal 方法,且最后一个参数为false,意思是如果key存在,则更新value值,返回旧val值。如果putVal最后一个参数为true如果key存在,则只返回旧val值。

下面是putVal(),刚开始看源码时不要看到函数就点进去一直点,更不要断点一步一步看,先看个大概思路

小白一定要看我写的注释!

final V putVal(K key, V value, boolean onlyIfAbsent) {
        //ConcurrentHashMap中key和value都不允许为空
        if (key == null || value == null) throw new NullPointerException();
        //key的哈希值,忍住好奇!先不看其原理
        int hash = spread(key.hashCode());
        //冲突次数 或者说 链表存放尝试次数
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {//相当于while true
            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) & hash)) == null) {//如果table对应位置为空,DCL机制存放到相应位置,并跳出循环
        //暂时省略下面代码...
        }
        addCount(1L, binCount);//map中的key个数+1
        return null;
    }

好的,上面我了解了没有冲突时候他是怎么放的,很简单,继续看。一定要看注释!

final V putVal(K key, V value, boolean onlyIfAbsent) {
        //ConcurrentHashMap中key和value都不允许为空
        if (key == null || value == null) throw new NullPointerException();
        //key的哈希值,忍住好奇!先不看其原理
        int hash = spread(key.hashCode());
        //冲突次数 或者说 链表存放尝试次数
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {//相当于while true
            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) & hash)) == null) {//如果table对应位置为空,DCL机制存放到相应位置,并跳出循环
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }//刚才看到这···································
            else if ((fh = f.hash) == MOVED)//先不看这,小本本上记一下,我这里疑惑。
                tab = helpTransfer(tab, f);
            else {//如果不走上面,一定会走这,意思是table数组,想放的哪个位置已经有人了
                V oldVal = null;//用于保存旧的value值
                synchronized (f) {//这里加了个锁,这个f是table[]的一个小格子,很微小,只锁定了hashcode相同的key的操作
                    if (tabAt(tab, i) == f) {//DCL机制,再来看看现在的f是不是之前的f,因为之前没加锁,有可能会不一样嘛
                        if (fh >= 0) {//fh就是f的hashcode值,上面有,为什么要判断这,小本本上记一下,我这里疑惑。
                            binCount = 1;//尝试次数为1了,因为放到table对应位置,发现不为null才来的这里
                            for (Node<K,V> e = f;; ++binCount) {//这个for循环没必要刚开始就仔细看,我猜它的作用就是把新的key放到链表尾部
                                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) {//把新的key放到链表尾部
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }//if (fh >= 0)的结束处
                        //else if ...我看了if了,先不看你
                    }
                }
                if (binCount != 0) {//发现这里判断binCount ,按照刚才的思路,binCount 肯定大于0,所以肯定会执行,那就看看
                    if (binCount >= TREEIFY_THRESHOLD)//点TREEIFY_THRESHOLD发现它是8,英文介绍说他是转成红黑树的阈值,和刚才的热身题对应起来了。吆西。
                        treeifyBin(tab, i);//转成红黑树,咋转的先不看,好奇心害死猫,但mark一下,是在这里转的
                    if (oldVal != null)//如果是覆盖了原来的值,那就返回原来的值
                        return oldVal;//返回
                    break;
                }
            }
        }
        addCount(1L, binCount);//map中的key个数+1
        return null;
    }

好的,如果上面明白了,那么,继续往下看,来看看刚才注释掉的else if 是干嘛的,一定要看注释!

                        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;
                            }
                        }

读者:嗯哼,putVal我搞定了,大概知道它咋做的了,我真牛逼!

对,是很厉害了,但这是个大概,我们刚才有个地方不是记在小本本上了?还不懂呢,嗯嗯,继续看,但是,先不看这。

再来看一下我刚才看过的这个putVal()中调用了哪些我不知道的函数,帮你们MARK列一下:

先从代码里看,有利于各位保持思路:

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());//············### MARK ### ···············
        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();//············### MARK ### ···············
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//············### MARK ### ···············
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);//············### MARK ### ···············
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        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;//············### MARK ### ···············
                            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);//············### MARK ### ···············
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);//············### MARK ### ···············
        return null;
    }

帮你们列好了,还有这些不懂呢:

  • spread(key.hashCode());------------------计算hash
  • initTable();------------------初始化,
  • tabAt(tab, i = (n - 1) & hash))------------------查看内存中最新值
  • casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))------------------用CAS的方式插入新结点
  • helpTransfer(tab, f);------------------???
  • putTreeVal(hash, key,value)--------------把新结点放到红黑树里
  • treeifyBin(tab, i);--------------------把 tab 第 i 个节点下的链表转成红黑树
  • addCount(1L, binCount);------------------执行完putVal,相当于count ++

本篇完成,下篇继续,喜欢点赞关注~

ConcurrentHashMap源码分析,轻取面试Offer(二)

注:本篇为CSDN--Star_Java原创,转载请注明出处!

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值