Jdk1.6和1.7版本中ConcurrentHashMap的弱一致性

前言:
昨天看技术论坛时看到了一篇<<为什么ConcurrentHashMap是弱一致的>>文章,其中的弱一致性是基于Jdk1.6版本的ConcurrentHashMap源码分析的,而在Jdk1.7和Jdk1.8中ConcurrentHashMap的设计都进行了一定程度的改良,是否仍然存在1.6版本中的弱一致性呢?
本文为了分析方便,假设有两个线程对一个ConcurrentHashMap实例进行并发处理,采用put-get这一并发操作组合来分析数据的一致性,put-get组合表示一个线程执行put方法时另一个线程执行get方法。另外,文章从1.6版本和1.7版本两个维度同时分析同一组合操作时的现象,进一步验证结论。
阅读此文章读者可能需要Java内存模型(JMM)、volatile内存语义、Happens Before关系、CAS、AQS、ConcurrentHashMap不同版本实现原理等基础前驱知识,文章只对一致性问题进行分析

在分析之前首先要知道ConcurrentHashMap在1.6和1.7中用volatile修饰的变量有哪些,如下表所示

1.6 1.7
Segment.count Segment.HashEntry[]
Segment.HashEntry[] Segment.HashEntry.value
Segment.HashEntry.value Segment.HashEntry.next

众所周知,ConcurrentHashMap使用锁分离技术,初始时有16个Segment段组成,一个Segment段包含一个类型为HashEntry的数组table,一个HashEntry元素又存在key-value的键值对,以及指向下一个HashEntry元素的指针next
所以,表中的Segment.count表示每个Segment段中HashEntry[]内元素的数量;Segment.HashEntry[]表示Segment段内的HashEntry[]数组;Segment.HashEntry.value表示一个HashEntry元素中的value值;Segment.HashEntry.next表示HashEntry链表中下一个元素

图1. 1.6版本put方法
图2. 1.6版本get方法

因为count被volatile修饰,因此标注1是对volatile的读操作,同理标注2也是对volatile修饰的table(HashEntry[])的读操作;根据Happens Before规则中的程序顺序规则(一个线程中的每个操作,happens-before于该线程中的任意后续操作),可以看出 1 happens before 2 happens before 3 happens before 4。其中,标注3是对普通变量的普通写操作,标注4是对volatile变量count的写操作;同样的道理在图2中存在5 happens before 6
那么我们模拟一下两个线程分别操作put方法和get方法的情况,首先是“正常”的情况

图3. 1.6版本下数据一致性正常的情况

图中因为标注1、3、4在同一线程中,因此存在happens before关系,另一个线程的5和6也存在happens before关系;此外,根据Happens Before规则中的volatile变量规则(对一个volatile域的写,happens-before于任意后续对这个volatile域的读)可以推理出4 happens before 5,因为4是对volatile变量count的写操作,5是对count的读操作。在根据Happens Before规则中的传递性(如果A happens-before B,且B happens-before C,那么A happens-before C),最终推出1 happens before 2 happens before 3 happens before 4 happens before 5 happens before 6,因此此时数据的一致性是得到保证的。那么我们再来看看弱一致的情况

图4. 1.6版本下数据弱一致性的情况

我们更改了4、5执行的顺序,一个线程先执行对volatile变量count的读操作,之后另一个线程再执行对count的写操作,这样4、5之间就不存在happens before关系了,并且上面分析过3的操作只是普通变量的读写操作,而5是对volatile变量table的读操作,因此3、5之间也不存在happens before关系,6中读取的并不是3处添加新HashEntry的最新table,这就导致了数据的弱一致性
下面我们来看看在Jdk1.7中ConcurrentHashMap的put和get方法

图5. 1.7版本put方法
图6. 1.7版本get方法

因为table为volatile修饰,因此标注1是一个volatile读,用一个局部非volatile修饰引用了volatile修饰的volatile,这里必须要注意一个知识点:volatile修饰的是reference,不是对象的实例,也就是说table指向了一个堆内内容为HashEntry[]内容的空间,这里的volatile修饰的是这个table在栈内的引用,不是栈内地址指向的堆内内容,而HashEntry<K,V>[] tab = table相当于又用了另一个变量tab指向了变量table指向的同一块内存地址,但是tab引用并没有被volatile修饰,所以tab是不具有volatile语义的相关特性的
标注2调用了HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i)方法,源码如下:

图7. entryAt方法

entryAt方法调用了UNSAFE类的getObjectVolatile,该方法是根据第二个参数的offset偏移量查找对应第一个参数中的值,该方法是支持volatile读语义的,其底层是在entryAt方法插入一个LoadLoad和一个LoadStore内存屏障防止CPU可能的指令重排序
接着再回去看图5中的标注3处,将新加入的HashEntry实例放在HashEntry[]特定的位置上,下面是其源码

图7 setEntryAt方法

同样的setEntryAt方法内部也调用了UNSAFE类的putOrderedObject方法,这里又存在一个坑,很多文章在分析该方法时都说其是一个具有volatile语义的方法,或者是否具有volatile语义依赖于第一个参数是否是volatile变量,但实际上putOrderedObject并不具有volatile语义,该方法的底层省去了volatile写的StoreLoad内存屏障,只添加了StoreStore内存屏障,所以只能保证putOrderedObject方法之前的内存可见性,不能保证数据的一致性,读者可以参考JUC中Atomic class之lazySet的一点疑惑,对该问题分析的非常漂亮,源码扒到了祖坟上
根据Happens Before规则中的程序顺序规则可以得出1 happens before 2 happens before 3,同理推出图6中4 happens before 5 happens before 6,但是对比图5和图6的代码发现,图5中和图6中只有table一个被volatile修饰的变量被共享,而且在put方法中table是volatile读,get方法中table也是volatile读,按照Happens Before规则中的volatile变量规则,必须存在另一个volatile的写,在这里也就是对于table变量的写,且写要在读之前才会行成Happens Before关系,很明显也不满足
我们再对比1.6版本中是如何完成“部分数据一致性”的,在1.6中count变量被volatile修饰了,因此该变量可以作为两个线程发生volatile的媒介,但在1.7版本中,count变量没有被volatile修饰,因此也不存在依靠该变量发生Happens Before关系的可能性。put方法和get方法中都存在对于局部变量tab的volatile操作,但经过逃逸性分析,这里的局部变量并不会逃逸到另一个线程中,所以也不会存在Happens Before语义
最后只剩标注4中的UNSAFE.getObjectVolatile(segments, u),上面分析过,虽然参数中的segments没有被volatile修饰,但是getObjectVolatile会强制在变量读取之后加上LoadLoadLoadStore内存屏障行成volatile读语义,但在put方法时也不存在对于该共享变量的volatile写操作,也就更谈不上行成Happens Before关系了。因此1.7版本ConcurrentHashMap的数据弱一致性也得以论证

后记
在写这篇文章的过程中发现很多之前“掌握”的知识其实非常肤浅,迫使我查阅各种资料和规范,也得到了很多大牛的帮助,即便写完文章也感觉有各种理解的不恰当的地方,摒弃浮躁,学无止境,永远以学生的心态脚踏实地前进

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容