ConcurrentHashMap1。7版本源码深度解读
ConcurrentHashMap是concurrent并发包下重要的工具类,它的设计和实现非常的巧妙,它将数据分段,每段数据使用一个AQS锁,从而减小了锁的粒度。1.ConcurrentHashMap的结构
结构图
一个ConcurrentHashMap是由多个Segment(段)组成的。Segment类继承了ReentrantLock类,这意味着每个Segment拥有了一个AQS,多个线程的操作落到同一个Segment上时,发生了锁的竞争。ConcurrentHashMap默认有16个Segment,在初始化之后,Segment个数不可修改。
一个Segment包含了一个HashEntry的数组,每个HashEntry都是一个单向链表,HashEntry的数组可以扩容,扩容后数组的长度为原来的2倍。HashEntry类如下图所示:
我们看到value和next都是volatile修饰的,这保证了数据的可见性。2.put方法详解public V put(K key, V value) { Segment s; if (value == null) throw new NullPointerException(); //计算key的hash值 int hash = hash(key); //hash为32位无符号数,segmentShift默认为28,向右移28位,剩下高4位 //然后和segmentMask(默认值为15)做与运算,结果还是高4位 int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment // 对 segment[j] 进行初始化 s = ensureSegment(j); //放入到对应的段内 return s.put(key, hash, value, false); }
第一步是根据hash值快速获取到相应的Segment,第二步就是Segment内部的put操作了。final V put(K key, int hash, V value, boolean onlyIfAbsent) { //获取该segment的独占锁 HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { HashEntry[] tab = table; //用hash值和(数组长度-1)做与运算,得出数组的下标 int index = (tab.length - 1) & hash; //first 是数组该位置处的链表的表头 HashEntry first = entryAt(tab, index); for (HashEntry e = first;;) { //判断是不是到了尾部,尾部==null if (e != null) { K k; //key相同,值更新 if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } //继续下一个节点 e = e.next; } else { //node不为空,则node作为头节点,使用的是头插法 if (node != null) node.setNext(first); else node = new HashEntry(hash, key, value, first); int c = count + 1; // 如果超过了该 segment 的阈值,这个 segment 需要扩容 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else // 没有达到阈值,将 node 放到数组 tab 的 index 位置, // 其实就是将新的节点设置成原链表的表头 setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
如何进行扩容:private void rehash(HashEntry node) { HashEntry[] oldTable = table; int oldCapacity = oldTable.length; //扩容为原来的2倍 int newCapacity = oldCapacity << 1; //计算阀值 threshold = (int)(newCapacity * loadFactor); HashEntry[] newTable = (HashEntry[]) new HashEntry[newCapacity]; int sizeMask = newCapacity - 1; //循环旧的HashEntry数组 for (int i = 0; i < oldCapacity ; i++) { HashEntry e = oldTable[i]; if (e != null) { HashEntry next = e.next; int idx = e.hash & sizeMask; //该链表上只有一个节点 if (next == null) // Single node on list newTable[idx] = e; else { // Reuse consecutive sequence at same slot HashEntry lastRun = e; int lastIdx = idx; for (HashEntry last = next; last != null; last = last.next) { //计算在新数组中的下标 int k = last.hash & sizeMask; //当前节点的下标和上一个节点的下标不一致时,修改最终节点值 //注意如果后面的节点和前面的节点下标一致, //那么后面的节点保持原有的顺序,直接带到新tab[k]的链表中 if (k != lastIdx) { lastIdx = k; lastRun = last; } } //采用头插法,最后一个节点作为头节点 newTable[lastIdx] = lastRun; //重新计算节点在数组中的位置,采用头插法插入到链表 for (HashEntry p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h & sizeMask; HashEntry n = newTable[k]; newTable[k] = new HashEntry(h, p.key, v, n); } } } } //添加新节点 int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; //将newTable赋值给该segment的table table = newTable; }
自旋获取aqs锁:private HashEntry scanAndLockForPut(K key, int hash, V value) { HashEntry first = entryForHash(this, hash); HashEntry e = first; HashEntry node = null; int retries = -1; // negative while locating node //如果尝试加锁失败,那么就对segment[hash]对应的链表进行遍历找到需要put的这个entry所在的链表中的位置, //这里之所以进行一次遍历找到坑位,主要是为了通过遍历过程将遍历过的entry全部放到CPU高速缓存中, //这样在获取到锁了之后,再次进行定位的时候速度会十分快,这是在线程无法获取到锁前并等待的过程中的一种预热方式。 while (!tryLock()) { HashEntry f; // to recheck first below //获取锁失败,初始时retries=-1必然开始先进入第一个if if (retries < 0) { //e=null代表两种意思, //1.第一种就是遍历链表到了最后,仍然没有发现指定key的entry; //2.第二种情况是刚开始时entryForHash(通过hash找到的table中对应位置链表的结点)找到的HashEntry就是空的 if (e == null) { /* 当然这里之所以还需要对node==null进行判断,是因为有可能在第一次给node赋值完毕后,然后预热准备工作已经搞定,然后进行循环尝试获取锁,在循环次数还未达到<2>64次以前,某一次在条件<3>判断时发现有其它线程对这个segment进行了修改,那么retries被重置为-1,从而再一次进入到<1>条件内,此时如果再次遍历到链表最后时,因为上一次遍历时已经给node赋值过了,所以这里判断node是否为空,从而避免第二次创建对象给node重复赋值。 */ if (node == null) // speculatively create node node = new HashEntry(hash, key, value, null); retries = 0; } else if (key.equals(e.key)) retries = 0; else e = e.next; } else if (++retries > MAX_SCAN_RETRIES) { // 尝试获取锁次数超过设置的最大值,直接进入阻塞等待,这就是所谓的有限制的自旋获取锁, //之所以这样是因为如果持有锁的线程要过很久才释放锁,这期间如果一直无限制的自旋其实是对cpu性能有消耗的, //这样无限制的自旋是不利的,所以加入最大自旋次数,超过这个次数则进入阻塞状态等待对方释放锁并获取锁 lock(); break; } // 遍历过程中,有可能其它线程改变了遍历的链表,这时就需要重新进行遍历了。 //判断是否初始化了结点 并且 判断链表头结点是否改变(1.7使用头插法) else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) { e = first = f; // re-traverse if entry changed retries = -1; } } return node; }
这个方法用了一个while循环去获取aqs锁,有两种情况需要说明下:
1.如果尝试的次数超过了最大自旋次数,会进入到aqs的等待队列,避免了cpu的空转。
2.如果在循环的过程中,其他的线程获取到了锁,并且改变了遍历的链表,那么自旋计数器重置为-1,从链表的头节点重新开始遍历。3.get方法public V get(Object key) { Segment s; // manually integrate access methods to reduce overhead HashEntry[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
get方法并没有加锁,基本思路是:
1.先定位到所在的segment
2.定位对应的segment的tab数组内的位置
3.然后遍历链表元素,如果找到相同的key就返回对应的value 4.总结:
ConcurrentHashMap1.7采用了分而治之的思想,将数据分段,每个段持有一把aqs锁。
它的写操作都是需要获取锁之后再操作,而读操作不需要获取锁,这也说明它适合读多写少的业务场景。
线程在获取不到aqs锁的情况下,不会立即进入到等待队列,会进行一定次数的自旋。
打算学编程,准备个什么牌子的笔记本电脑比较好?目前来讲看中了苹果和华为的?先补充下背景,在编程界,编程设备电脑,有两个世界,一个世界是普通世界,这个世界里,程序员写代码的电脑和大众玩游戏看电影上网做ppt的电脑一样,就是你手头的普通电脑,什么电脑都行。另
电脑界有两种说法高U低显和低U高显你们觉得哪个合理?电脑界有两种说法高U低显和低U高显你们觉得哪个合理?要把这个问题弄清楚,我们首先就要来确认一下什么叫高U低显和低U高显?高U低显这个配置的搭配就是说用一个档次相当高的处理器搭配一个
投资房产是普通人唯一应对通胀的方法吗?在过去20年,投资房产是普通人唯一应对通胀的方法。能够容纳民间海量资金的蓄水池,目前一般只有三个银行存款,购买房产,投资股市。不管是现在,还是过去,银行存款显然是没有办法抵抗通胀的
怀孕期间吃什么会让孩子既聪明又皮肤好?怀孕期间其实吃什么,只要注意量,营养均衡,就是对宝宝最好的。不过,研究发现,有一些食物,的确有特殊的功效。西兰花西兰花它含有丰富的维生素A维生素C和胡萝卜素,能增强皮肤的抗损伤能力
如何解决年轻人不愿生孩子的问题?国家应出台政策降低房价,均衡教育资源,降低教育成本。使育龄夫妇生孩后,养得起。大家都很重视优生优育。谁说年轻人不愿意生孩子?和日本韩国美国新西兰的年轻人比,我能的年轻人还是愿意生孩
新生儿抚触有用吗?抚摸可以改变孩子的基因表达。对于人类而言,在小孩子的成长过程中,父母也应多抚摸孩子。心理学家很早就注意到了,来自父母的抚摸,可以让孩子更有安全感,使其性格更加的稳定,但直到最近科学
云顶之弈中怎么区分物理伤害和魔法伤害?其实我也有这个疑惑很久了,英雄界面上也没有写一个英雄到底是物理的还是魔法的,就写着造成多少多少的伤害,那到底是物理还是魔法啊?韦鲁斯出一个帽子怎么样啊?后来经过我的苦心专研后终于明
休斯顿世乒赛再次败北,是不是再次证明伊藤美诚不是中国队威胁?伊藤美诚少女得志崭露头角,惊出国乒一身冷汗。坏事变好事,中国征对性伊藤美诚这个活教材,激起了强大的中国乒乓球女队。孙颖莎与王曼昱要抜掉最后的一个钉子,就是日本女子双打伊藤美诚早田希
NBA最经典的十大未解谜团都有什么?张伯伦到底有没有20000个女朋友?罗德曼为什么突然就长高一大截?马龙罚球的时候嘴里念的是什么咒语?老话说的好有人的地方,就会有故事。作为全球商业化及竞技水平最高的篮球联赛,NBA
为什么邓亚萍张怡宁对孙颖莎女单决赛评价大不相同?世乒赛已经结束,王曼昱击败孙颖莎夺得女单冠军。针对孙颖莎在本次半决赛和决赛中的表现,张怡宁邓亚萍两位大魔王也是给出了自己的评价。两人给出的评价大不相同,邓亚萍认为孙颖莎正手的稳定性
你是怎样应对七到九岁孩子叛逆不听话的?七八岁的娃娃不听话,主要原因是他们正在探索自己做主,是他们的第二个叛逆期,作为父母应该感到高兴,因为孩子正在尝试独立做主,为什么呢?老话说得好,三岁看大,七岁看老。这时候可以看到孩
河钢集团唐钢公司强化成本意识蓄力再谱新篇河钢集团唐钢公司动力部职工在3号TRT(高炉煤气余压透平发电机组)进行巡检,保障设备高效运行。长城网冀云客户端讯(记者杜宇昕通讯员赵辉付会文)连日来,河钢集团唐钢公司动力部认真贯彻
网购有风险诉讼保权益天水日报讯(新天水天水日报记者裴婷婷)随着网络的普及运用,网络购物已成为人们的主要消费方式之一,但在享受网络购物便利时,出现售后问题和纠纷又该如何解决呢?近日,秦安县人民法院就审理
每日汇市上涨超300基点!人民币对美元中间价调升344基点,报6。9131北京商报讯(记者廖蒙)1月4日,央行授权中国外汇交易中心公布,2023年1月4日银行间外汇市场人民币汇率中间价为1美元对人民币6。9131元,前一交易日中间价报6。9475元,单日
2023年养老金调整,企退和事退人员增长水平一样吗?没那么简单2023年养老金即将调整了。根据社会保险法规定,我们要根据社会平均工资增长和物价增长情况,适时调整提高退休人员基本养老保险待遇情况。去年13季度,人均可支配收入中的的工资性收入增长
传言苹果砍单果链概念重挫1月4日,A股多只苹果概念股股价大跌,其中果链龙头立讯精密(002475)盘中一度跌近9,东山精密鼎阳科技云海金属等盘中跌逾5。苹果概念股股价大跌可能与市场上一则传闻有关,有媒体报
GAP在华易主电商代运营求转型将GAP收入麾下对宝尊电商来说是良药还是拖累?截至1月4日,宝尊电商股价多日上涨,涨超6。资本投票的背后,或源于宝尊电商接盘GAP推出品牌管理业务等一系列变革。事实上,随着电商红利
估值45亿!重整后的汇源,国中水务的救星?出品达摩财经重整中的老牌饮料品牌汇源,依然是一个值得期待的资产。2022年12月26日,南通首富姜照柏控制的国中水务(600187。SH)公告称,计划受让文盛资产参与北京汇源重整设
2023年,3家公司和3条路径大树底下只有苔藓。谁能离开苹果特斯拉和华为的路径影响力?文智物一hr先从苹果聊起比较安全,苹果公司就像郭德纲的相声一样,是一种幽默的尺度。凡是跟郭德纲认真吵架的人,一张嘴就输了。同
6款备孕营养汤!喝出好状态,备孕没烦恼!备孕期间,营养充足身体精神状态好的朋友是不必纠结于各类大补的。饮食以安全营养均衡为主,日常多吃蛋白质丰富的鱼虾肉蛋新鲜的蔬菜水果,以及补充能量的主食和适量油脂就可以,不需要刻意的大
NT是什么?NT是什么?NT检查又称颈后透明层扫描,通过B超检查测量胎儿颈项部皮下无回声透明层最厚的部位,用于评估胎儿是否患有唐氏综合征。研究表明,早孕期胎儿NT增厚跟胎儿染色体异常胎儿先天性
古镇镇机关第一幼儿园家长开放活动隆重举行古镇镇机关第一幼儿园是古镇镇政府举办的省一级标准的公办幼儿园,与中山市机关第一幼儿园集团化办学。2022年秋季开学以来,幼儿园得到社会各界人士和幼儿家长的高度认可。近日,幼儿园开展