上篇ConcurrentHashMap源码分析,轻取面试Offer(一)中降到了看源码的方法,下面接上篇继续分析源码

先来上篇注释过的代码段和遗留的问题。

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 trueNode<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 (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) {//发现这里判断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个数+1return 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 ++

下面要做的,不是看你不懂的函数helpTransfer,既然不懂,看了也糊,从懂的地方看,最后回过头来看这。

spread函数:

static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;//HASH_BITS的值为0x7fffffff}

发现他就是把 key 的 hashCode() h,将其与 自己的高16位进行异或运算,与HASH_BITS进行与运算消除负hash。

initTable()函数

/*** Initializes table, using the size recorded in sizeCtl.*/private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {if ((sc = sizeCtl) < 0)//为什么判断他小于0?不懂,继续往下看Thread.yield(); // 线程让步,有前人正在初始化,退让,下次获取权限就退出该函数了else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//通过CAS的方式,如果sc=0,就把它置为-1try {if ((tab = table) == null || tab.length == 0) {//DCL 双重验证int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//sc如果小于0,则置为16,按照我们当前的思路,这里肯定大于0,没变相当于,!sizeCtl可以表示初始化的大小@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//创建了~table = tab = nt;sc = n - (n >>> 2);//sc的值改变为 n - 0.25n,也就是 0.75n}} finally {sizeCtl = sc;//sc 赋值给 sizeCtl !sizeCtl可以表示阈值,}break;}}return tab;}

tabAt函数

@SuppressWarnings("unchecked")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);}

这个函数的意思即为,查看当前内存中最新的 table[i] 的值

这里有的小白会问table不是volatie修饰的么?为什么还要这样去看?这不是多次一举么?

答:
table确实是volatie的,这只是代表数组的引用是内存可见的,但不代表引用地址指向的内容是可见的

(maspaint简单画了一下)如图:

因此需要用这种方式来查看内存中最新的。

casTabAt

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

用cas的方式来放进去。

helpTransfer,这个先放放,剧透一下,这个是帮助其他线程进行扩容操作的。

??
还有这种操作,66

putTreeVal

在红黑树中添加一个节点的操作我想你们应该都懂,我就不放了。不懂的同学我相信看了会更晕,就不放了先

treeifyBin

private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {if ((n = tab.length) < MIN_TREEIFY_CAPACITY)//MIN_TREEIFY_CAPACITY=64,只有table.length 不< 64 时候,他才会转成红黑树tryPresize(n << 1);//tab.length < 64 进行双倍扩容(左移一位)else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {synchronized (b) {if (tabAt(tab, index) == b) {//DCL 检查TreeNode<K,V> hd = null, tl = null;//下面是转红黑树的操作,对算法感兴趣的可以看看,看思路务必先略过for (Node<K,V> e = b; e != null; e = e.next) {TreeNode<K,V> p =new TreeNode<K,V>(e.hash, e.key, e.val,null, null);if ((p.prev = tl) == null)hd = p;elsetl.next = p;tl = p;}setTabAt(tab, index, new TreeBin<K,V>(hd));//这里又多了个函数}}}}}

看setTablAt

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);}

与上面类似,为了安全才这样做

····重点来了,敲黑板!!!

addCount 大体就是让count++,并且如果table不够大,那就扩若,如果已经在扩容了,那就去帮着扩容

在看这个函数前,想想之前CAS的知识

自旋锁是有可能失败的,所以他是unsafe底下的方法,ConcurrentHashMap为了让它变成安全,Doug Lea加了两个内存可见的变量:

    /*** Base counter value, used mainly when there is no contention,* but also as a fallback during table initialization* races. Updated via CAS.
翻译:
基本计数器值,主要用于没有争用时,但也用作表初始化竞赛期间的后备值。通过CAS更新*/private transient volatile long baseCount;//也就是这个变量是存储当前一共有多少key,(但不一定准确,因为CAS 时候有可能失败,该变量未统计CAS失败的个数)/*** Table of counter cells. When non-null, size is a power of 2.*/private transient volatile CounterCell[] counterCells;//CAS计数失败时候保存在这里//可以猜出来counterCells不为null之前已经有过baseCount CAS失败,发生失败代表并发不低//baseCount CAS失败时 则在counterCells数组中使用随机数随便取一个索引位置之前记录的数据进行数量累加CAS
//这也解释了为什么额外的计数器采用数组而不是int之类的,因为CAS的机制,并发太高时候他就容易失败,采用数组做缓冲。
addCount(long x, int check)/*
····第一个参数 x
这个函数有两个参数,第一个参数为long 也就是add计数的个数,来复习一下我们当时怎么调用的它:
*/
addCount(1L, binCount);
//因为我们这个put就put了一个,所以是1L,
/*为啥是长整型?
put还能放map呢,还有并发等等,有问题可以先记下来,后面再想,先跟着我的思路····第二个参数 check
我们当时调用的时候传过来的是binCount,调用put插入时>=0,直接放在table上为0,否则按链表或者红黑树次序为 1,2,3,4....以此类推,上一博客中提到了。
扩展:
删除或清理节点时(如replaceNode,clear方法,)传过来的是-1
*/

继续往下看,先上jdk源代码,小白先别细看,只看我注释就好!!贴上来为了让大家对源码长什么样有个数:

/*** Adds to count, and if table is too small and not already* resizing, initiates transfer. If already resizing, helps* perform transfer if work is available.  Rechecks occupancy* after a transfer to see if another resize is already needed* because resizings are lagging additions.** @param x the count to add* @param check if <0, don't check resize, if <= 1 only check if uncontended翻译:
*添加计数
如果表太小且尚未调整大小,则调用transfer。
如果正在调整大小,且还有活干,那就去帮忙去。
在转移之后重新检查占用率,看看是否已经需要再调整大小,因为调整大小是滞后的添加。*@ PARAM-X要添加的计数* @ PARAM检查是否为0,不检查调整大小,如果<=1只检查是否未争用大体看一眼,先知道大概有几个if ,不用细看,会晕车。*/private final void addCount(long x, int check) {CounterCell[] as; long b, s;if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {//······### Mark ###·······CounterCell a; long v; int m;boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[ThreadLocalRandom.getProbe() & m]) == null ||!(uncontended =U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {fullAddCount(x, uncontended);return;//······### Mark ###·······}if (check <= 1)//······### Mark ###·······return;s = sumCount();}if (check >= 0) {Node<K,V>[] tab, nt; int n, sc;while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {int rs = resizeStamp(n);//······### Mark ###·······if (sc < 0) {//······### Mark ###·······if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))transfer(tab, nt);}else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);//······### Mark ###·······s = sumCount();}}}

跟着我的思路继续看,我们是从putVal函数进来的,当时addCount(1L, binCount);这么调用的,现在假设binCount = 0

private final void addCount(long x, int check) {CounterCell[] as; long b, s;
//并发量很高才会进入这里,这里我们先想竞争不大,略过ifif (...) {//...忽略}if (check >= 0) {//我们put方法会进来,所以继续看Node<K,V>[] tab, nt; int n, sc;//s是ConcurrentHashMap中实际元素个数,sum 简写,sizeCtl 是阈值while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&(n = tab.length) < MAXIMUM_CAPACITY) {//while循环判断,当前元素个数 > 阈值 且 table 不为空,且tab.length小于MAXIMUM_CAPACITY(MAXIMUM_CAPACITY 的值为 1 << 30)int rs = resizeStamp(n);//···Mark···这里自适应调整新table的大小为合适的2的次幂,比如n是15,那rs就是32if (sc < 0) {//sc=sizeCtl初始是阀值,sc肯定大于0,先不看这//...省略},else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))transfer(tab, null);//启动transfer,因为扩容需要把之前的table中的元素都转移到新table中s = sumCount();sumCount是正真的计算map元素数量的方法:计算baseCount和counterCells数组存的总和,意思是每次循环都是获取最新值}}}

看了上面提到的,想必各位对ConcurrentHashMap的putVal操作有了大概的了解,大概知道他是怎么放得,这时候需要自己点源码顺着上面的思路过一遍,一定要再多看几遍!

继续探讨,请看下篇:ConcurrentHashMap源码分析,轻取面试Offer(三)

本章伏笔:

  • 第一个if干啥了?
  • 为啥是check <= 1?
  • resizeStamp(n); 这么神奇?这个函数咋做到的?
  • sc < 0? 还能小于0?
  • transfer(tab, null); ------------扩容,干了啥?
  • 你说的多个线程帮忙扩容怎么帮的?

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

ConcurrentHashMap源码分析,轻取面试Offer(二)相关推荐

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

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

  2. 【阅读源码系列】ConcurrentHashMap源码分析(JDK1.7和1.8)

    个人学习源码的思路: 使用ctrl+单机进入源码,并阅读源码的官方文档–>大致的了解一下此类的特点和功能 使用ALIT+7查看类中所有方法–>大致的看一下此类的属性和方法 找到重要方法并阅 ...

  3. ConcurrentHashMap源码分析(1)——JDK1.7的实现

    ConcurrentHashMap源码分析 ConcurrentHashMap源码分析(2)--JDK1.8的实现 前言 ConcurrentHashMap是线程安全且高效的HashMap的实现,在并 ...

  4. ConcurrentHashMap源码分析(2)——JDK1.8的实现

    ConcurrentHashMap源码分析(1)--JDK1.7的实现 前言 在JDK1.7版本上,ConcurrentHashMap还是通过分段锁来实现的,Segment的数量制约着并发量.在JDK ...

  5. 手机自动化测试:appium源码分析之bootstrap十二

    手机自动化测试:appium源码分析之bootstrap十二 poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请 ...

  6. Spring 源码分析(四) ——MVC(二)概述

    随时随地技术实战干货,获取项目源码.学习资料,请关注源代码社区公众号(ydmsq666) from:Spring 源码分析(四) --MVC(二)概述 - 水门-kay的个人页面 - OSCHINA ...

  7. 多线程高并发编程(10) -- ConcurrentHashMap源码分析

    一.背景 前文讲了HashMap的源码分析,从中可以看到下面的问题: HashMap的put/remove方法不是线程安全的,如果在多线程并发环境下,使用synchronized进行加锁,会导致效率低 ...

  8. Java源码详解六:ConcurrentHashMap源码分析--openjdk java 11源码

    文章目录 注释 类的继承与实现 数据的存储 构造函数 哈希 put get 扩容 本系列是Java详解,专栏地址:Java源码分析 ConcurrentHashMap 官方文档:ConcurrentH ...

  9. [JUC-5]ConcurrentHashMap源码分析JDK8

    在学习之前,最好先了解下如下知识: 1.ReentrantLock的实现和原理. 2.Synchronized的实现和原理. 3.硬件对并发支持的CAS操作及JVM中Unsafe对CAS的实现. 4. ...

最新文章

  1. Max_user_connections 与Max_connections 与max_connect_errors
  2. Metasploit irb命令使用技巧
  3. python 二分法实现pow_Python实现二分法和黄金分割法
  4. Condition_number
  5. 阻塞、非阻塞、超时(同步与异步)
  6. extmail垃圾邮件存放垃圾邮件箱
  7. Python-OpenCV基本操作
  8. 解决window.location.href 下载文件时,一次点击产生两次下载+页面跳转问题
  9. Android入门学习3
  10. android ListView的怪异现象
  11. 用计算机找到自己的另一半,生辰八字算婚期计算器 免费算个人感情婚姻的另一半...
  12. 复习一下forearch
  13. ACM、OI、IOI编程竞赛模式介绍
  14. Win10安全证书过期怎么办
  15. 怎么设置cmd 以管理员身份运行
  16. 分享Silverlight/WPF/Windows Phone一周学习导读
  17. 超酷震撼 HTML5/CSS3动画应用及源码
  18. win10计算机管理 用户,Win10专业版系统管理员帐户的开启设置方法
  19. 方舟无限琥珀服务器,方舟生存进化无限琥珀版
  20. python判断奇数和偶数_从Python中的给定列表中提取偶数和奇数

热门文章

  1. 实现病案首页数据上报自动化-小帮全面解决-数据上报自动化
  2. 数据库分类和负载均衡方案
  3. 某精英枪战游戏辅助脚本加密破解
  4. 会员自动续费服务协议
  5. 项目部署后的域名配置
  6. 2019年8月7日暑假训练
  7. ������ʾ����
  8. Ubuntu20.04安装中国版firefox
  9. uml 菱形_UML图符号的含义
  10. Obi Fulid对于URP支持注意事项