集合源码之ConcurrentHashMap

引子

上篇解析过的hashmap不是线程安全的,因此并发大师Doug Lea在jdk1.5增加了concurrenthashmap类以满足开发者在多线程环境安全使用map的需要。concurrenthashmap作为多线程环境常使用的类,它是如何实现线程安全的?但是又为什么说它不能完全替代hashtable?弱一致性又是怎样一回事?读完这篇解析concurrenthashmap的文章,我相信你会对concurrenthashmap的方方面面有一定的了解。
涉及到源码的分析,一般我们都是先分析数据结构,然后在分析算法,数据结结构和算法都理解了,那么你就了解了大部分内容。类似上篇hashmap,对concurrenthashmap的分析也分为三个部分:

  • 第一部分:分析探究concurrenthashmap的数据结构。
  • 第二部分:分析concurrenthashmap的重要算法实现。
  • 第三部分:探究concurrenthashmap值得探讨的问题。

为了简单,跟hashmap类似的部分本文将一笔带过,如果没看过hashmap源码可以看上篇文章集合源码之hashmap,hashmap是concurrenthashmap的基础,不建议没读过hashmap直接看本篇文章。本文分析的源码采用的是concurrenthashmap较为简单的jdk1.6版本。

concurrentHashMap的数据结构

首先我们来看concurrenthashmap数据结构的图示,有个整体的认识。



图里的是主要组成表示,我们更加具体的去分析concurrenthashmap的数据结构要分为concurrenthashmap的数据结构、segment的数据结构以及hashEntry的数据结构。重要的数据结构我们会解析用途以便大家更好的理解算法实现部分。

常量

  • DEFAULT_INITIAL_CAPACITY:默认初始容量16,与hashmap相同。
  • DEFAULT_LOAD_FACTOR:默认装填因子0.75
  • DEFAULT_CONCURRENCY_LEVEL:默认并发级别16。其实就是segment个数,那为什么叫并发级别而不是segment数量呢?这里有必要细讲一下,我们知道concurrenthashmap是通过分段锁来实现高效并发的,这里的锁就是指segment,每个segment持有一把锁管理自身的数据。所以有几个segment就有几把锁,也就是几的并发级别。另外,segment初始化之后数量不会再改变。
  • MAXIMUM_CAPACITY:最大容量。
  • MAX_SEGMENTS:最大并发级别。
  • RETRIES_BEFORE_LOCK:查询操作无锁获取尝试次数。即某些查询操作先不加锁获取,如果获取的结果不对(获取期间concurrenthashmap值变化)重试,如果超过尝试次数,再进行加锁获取操作。

实例变量

  • segmentMask/segmentShift:计算对应的segment索引使用。
  • segments:对应的segment数组。
  • keySet/entrySet/values:对应的key、entry set集合。值的values集合。

segment

我们有必要对segment单独的进行数据结构分析,因为主要的算法实现都是在segment中,segment是concurrenthashmap的灵魂。可以把segment看做是线程安全的hashmap,因为数据结构与hashmap非常相似。

  • 继承 ReentrantLock:因此segment具有锁的特性,可以对自身进行加锁解锁等操作。
  • count/modCount/threshold/loadfactor:数量、修改次数、扩容临界值、装填因子。
  • hashEntry[]:真正存储数据的节点类数组。数据结构如开篇图示。

concurrenthashmap重要算法实现

正如上文提到的,concurrenthashmap的灵魂是segment,segment的算法实现是整个concurrenthashmap的基础,我们会着重分析segment的实现。

构造方法

多个构造方法,只讨论最核心的构造方法。主要分为下面几个步骤:

  • 校验参数:校验入参初始容量、装填因子、并发级别合法性。
  • 根据并发级别确定segment数组大小:segment也得是2的指数大小。segmentmask为segment的大小-1,segmentshift为32减去调整并发级别次数。
  • 初始化segment:这里有段代码值得讨论。
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = 1;
        while (cap < c)
            cap <<= 1;

        for (int i = 0; i < this.segments.length; ++i)
            this.segments[i] = new Segment<K,V>(cap, loadFactor);

这段代码计算cap的最终目的是为了确认初始化segment大小,让cap与segment总数的乘积要大于等于初始容量的大小。

重要算法

需要说明的是除了全局的算法,大部分算法都是确定是哪个segment,然后调用segment方法。分为四类探讨增删改查,另外不接受null的value。

  • isEmpty():需要校验所有的segment,所有算法实现要考虑的比hashmap要多。主要分为两步:初次校验、再次校验。这两步很多方法用到,需要着重理解。
    • 初次校验: 遍历所有segment,如果count不为0返回false,并记录每个segment的modcount以及所有的modcount以便再次校验。
    • 再次校验:总modcount为0说明为空。不为0说明有值,那么就可能存在初次校验时发生值修改的情况,需要再次校验。遍历segment,如果count不为0以及每个modcount发生修改就返回false。
  • size():即所有segment的count和。先不加锁获取,失败加锁获取。
    • 不加锁获取:循环次数由RETRIES_BEFORE_LOCK决定。先遍历segment记录和、每个modcount以及总modcount。如果modcount不为0需要再次校验,校验时modcount不符则强制加锁读取,否则获取第二次校验和。两次和相同即为size,不同再次循环。
    • 加锁获取:两次的和不同的前提。遍历segment加锁、遍历取和、遍历解锁。返回和值。
  • get(key):根据hash值找到对应的segment,然后调用segment的get方法。
    • segment---get(key,hash):如果count为0直接返回null。根据hash值得到对应entry的首节点,遍历节点,没找到返回null,找到了返回对应的value。但是这里需要注意的是get是不加锁操作,可能get时有线程修改value导致对应key的value为null,这时候需要加锁读取了(readValueUnderLock(e))
  • containskey(key):根据hash值找到对应segment,调用contain方法
    • segment---containsKey(key,hash):如果count为0直接返回false。根据hash值得到对应entry数组索引的首节点。遍历节点,存在返回true,否则返回false。
  • containsValue(value):因为value存在的segment不确定,需要遍历所有的segment。与size的操作相同,先不加锁执行,然后如果需要在加锁执行。主要分为三步:
    • 不加锁读取:遍历segment,调用segment的containsvalue方法,包含返回true,否则执行不加锁校验方法。
    • 不加锁校验:总modcount不为0,校验每个segment的modcount,如果有不相同说明值发生改变需要加锁读取,如果modcount没变说明不包含指定value,返回false。
    • 加锁读取:遍历segment加锁。遍历segment调用containsvalue,存在返回true,否则返回false,遍历解锁。
增(put(key,value)/putIfAbsent(key,value))

增加方法也是调用segment的put(key,hash,value,boolean onlyIfAbsent)。onlyifabsent如果为true,则如果segment存在key不替换value。put默认替换value。put方法先lock,然后进行put操作。put方法跟hashmap类似,此处不赘述。

删(remove(key)/remove(key,value))

删除方法也是调用segment的remove(key,hash,value)方法。先找到要删除的节点,找到后删除节点,但是这里的删除跟hashmap中的有所不同,因为hashentry的next是final的不可改变,所以只能新建节点,next指向删除节点的next,然后克隆删除节点前继节点,这样就完成了删除操作。
另外clear方法是遍历segment,调用segment的clear方法。clear方法加锁,遍历hashentry数组置null。

修改方法有两个,一个是replace(key,value)调用segment的replace(key,hash,value),另一个是replace(key,oldvalue,newvalue)调用segment的replace(key,hash,oldvalue,newvalue)。
- segment---replace(key, hash, value):先加锁,然后遍历节点找到对应的节点,替换value。如果不存在key对应的节点就返回null。
- segment---replace(key, hash,oldvalue,newvalue):跟上面的方法类似,不同的是如果节点里的value跟传入的oldvalue不同也不进行替换。

深入探讨

concurrenthashmap的弱一致性

什么叫弱一致性?理想的强一致性就是放入一个元素后,立即获取元素就是刚放入的元素,但是concurrenthashmap可能某时间段看不到这个元素。如下图两个线程执行顺序:

则刚放入的元素可能会取不到。详细的可以参考文章为什么ConcurrentHashMap是弱一致的

与hashtable、collections封装后的SynchronizedMap比较

其实还可以加入concurrenthashmap的segment。

  • hashtable:使用synchronized关键字对方法进行修饰。
  • SynchronizedMap:使用一个mutex对象作为锁,每次访问方法必须要获取mutex对象的锁。
  • segment:使用ReentrantLock作为锁,访问方法时lock进行加锁
  • concurrenthashmap:多个segment。
    一对比就可以看出concurrenthashmap引入并发级别的策略,可以让concurrenthashmap在多线程环境的性能更加突出,速度也更快。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 前言 JDK中的Hashtable是一个线程安全的K-V形式的容器,它实现线程安全的原理十分简单,就是在所有涉及对...
    Justlearn阅读 2,306评论 0 11
  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,485评论 0 3
  • ConcurrencyMap 从这一节开始正式进入并发容器的部分,来看看JDK 6带来了哪些并发容器。 在JDK ...
    raincoffee阅读 553评论 0 0
  • 引言 ConcurrentHashMap是线程安全并且高效的HashMap,在并发编程中经常可见它的使用,在开始分...
    miaoLoveCode阅读 15,822评论 14 40
  • 水平不高,但是喜欢写字,比如签名、短句…… 如果喜欢的话,评论名字帮写,欢迎打扰!新人求关注啦~~
    社会小学生阅读 688评论 12 4