前些天发现了一个巨牛的人工智能学习网站通俗易懂风趣幽默忍不住分享一下给大家。点击跳转到网站。本文将用到Java内存模型的happens-before偏序关系下文将简称为hb以及ConcurrentHashMap的底层模型相关的知识。happens-before相关内容参见JLS §17.4.5. Happens-before Order、深入理解Java内存模型以及Happens beforeConcurrentHashMap的详细介绍以及底层原理见深入分析ConcurrentHashMap。本文将从ConcurrentHashMap的getcleariteratorentrySet、keySet、values方法三个方法来分析它们的弱一致问题。ConcurrentHashMap#getget方法是弱一致的是什么含义可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后立马能对get可见但ConcurrentHashMap并不能如你所愿。换句话说put操作将一个元素加入到底层数据结构后get可能在某段时间内还看不到这个元素若不考虑内存模型单从代码逻辑上来看却是应该可以看得到的。下面将结合代码和java内存模型相关内容来分析下put/get方法本文中所有ConcurrentHashMap相关的代码均来自hotspot1.6.0_18。put方法我们只需关注Segment#putget方法只需关注Segment#get在继续之前先要说明一下Segment里有两个volatile变量count和tableHashEntry里有一个volatile变量value。Segment#putV put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c count; if (c threshold) // ensure capacity rehash(); HashEntryK,V[] tab table; int index hash (tab.length - 1); HashEntryK,V first tab[index]; HashEntryK,V e first; while (e ! null (e.hash ! hash || !key.equals(e.key))) e e.next; V oldValue; if (e ! null) { oldValue e.value; if (!onlyIfAbsent) e.value value; } else { oldValue null; modCount; tab[index] new HashEntryK,V(key, hash, first, value); count c; // write-volatile } return oldValue; } finally { unlock(); } }Segment#getV get(Object key, int hash) { if (count ! 0) { // read-volatile HashEntryK,V e getFirst(hash); while (e ! null) { if (e.hash hash key.equals(e.key)) { V v e.value; if (v ! null) return v; return readValueUnderLock(e); // recheck } e e.next; } } return null; }我们如何确定线程1放入某个变量的值是否对线程2可见文章开头提到的JLS链接中有说到当a hb c时a对c可见那么我们接下来我们只要寻找put和get之间所有可能的执行轨迹上的hb关系。要找出hb关系我们需要先找出与hb相关的Action。为方便这里将两段代码放到了一张图片上。可以注意到同一个Segment实例中的put操作是加了锁的而对应的get却没有。根据hb关系中的线程间Action类别可以从上图中找出这些Action主要是volatile读写和加解锁也就是图中画了横线的那些。put操作可以分为两种情况一是key已经存在修改对应的value二是key不存在将一个新的Entry加入底层数据结构。key已经存在的情况比较简单即if (e ! null)部分前面已经说过HashEntry的value是个volatile变量当线程1给value赋值后会立马对执行get的线程2可见而不用等到put方法结束。key不存在的情况稍微复杂一些新加一个Entry的逻辑在else中。那么将new HashEntry赋值给tab[index]是否能立刻对执行get的线程可见呢我们只需分析写tab[index]与读取tab[index]之间是否有hb关系即可。假设执行put的线程与执行get的线程的轨迹是这样的tab变量是一个普通的变量虽然给它赋值的是volatile的table。另外虽然引用类型数组类型的变量table是volatile的但table中的元素不是volatile的因此⑧只是一个普通的写操作count变量是volatile的因此②是一个volatile写③很显然是一个volatile读⑨中getFirst方法中读取了table因此包含一个volatile读。根据Synchronization Order对同一个volatile变量有volatile写 hb volatile读。在这个执行轨迹中时间上②在③之前发生且②是写count③是读count都是针对同一个volatile变量count因此有② hb ③又因为⑧和②是同一个线程中的③和⑨是同一个线程中的根据Program Order有⑧ hb ②③ hb ⑨。目前我们有了三组关系了⑧ hb ②② hb ③③ hb ⑨再根据hb关系是可传递的即若有x hb y且y hb z可得出x hb z可以得出⑧ hb ⑨。因此如果按照上述执行轨迹⑧中写入的数组元素对⑨中的读取操作是可见的。再考虑这样一个执行轨迹这里只是变换了下执行顺序。每条语句的volatile读写含义同上但它们之间的hb关系却改变了。Program Order是我们一直拥有的即我们有⑧ hb ②③ hb ⑨。但这次对volatile的count的读时间上发生在对count的写之前我们无法得出② hb ⑨这层关系了。因此通过count变量在这个轨迹上是无法得出⑧ hb ⑨的。那么存不存在其它可替换关系让我们仍能得出⑧ hb ⑨呢我们要找的是在⑧之后有一条语句或指令x在⑨之前有一条语句或指令y存在x hb y。这样我们可以有⑧ hb xx hb y y hb ⑨。就让我们来找一下是否存在这样的x和y。图中的⑤、⑥、⑦、①存在volatile读写但是它们在⑧之前因此对确立⑧ hb ⑨这个关系没有用处同理④在⑨之后我们要找的是⑨之前的因此也对这个问题无益。前面已经分析过了②③之间没法确立hb关系。在⑧之后我们发现一个unlock操作如果能在⑨之前找到一个lock操作那么我们要找的x就是unlock要找的y就是lock因为Synchronization Order中有unlock hb lock的关系。但是很不幸运⑨之前没有lock操作。因此对于这样的轨迹是没有⑧ hb ⑨关系的也就是说如果某个Segment实例中的put将一个Entry加入到了table中在未执行count赋值操作之前有另一个线程执行了同一个Segment实例中的get来获取这个刚加入的Entry中的value那么是有可能取不到的此外如果getFirst(hash)先执行tab[index] new HashEntryK,V(key, hash, first, value)后执行那么这个get操作也是看不到put的结果的。……正是因为get操作几乎所有时候都是一个无锁操作get中有一个readValueUnderLock调用不过这句执行到的几率极小使得同一个Segment实例上的put和get可以同时进行这就是get操作是弱一致的根本原因。Java API中对此有一句简单的描述:Retrievals reflect the results of the most recently completed update operations holding upon their onset.也就是说API上保证get操作一定能看到已完成的put操作。已完成的put操作肯定在get读取count之前对count做了写入操作。因此也就是我们第一个轨迹分析的情况。ConcurrentHashMap#clearclear方法很简单看下代码即知。public void clear() { for (int i 0; i segments.length; i) segments[i].clear(); }因为没有全局的锁在清除完一个segments之后正在清理下一个segments的时候已经清理segments可能又被加入了数据因此clear返回的时候ConcurrentHashMap中是可能存在数据的。因此clear方法是弱一致的。ConcurrentHashMap中的迭代器ConcurrentHashMap中的迭代器主要包括entrySet、keySet、values方法。它们大同小异这里选择entrySet解释。当我们调用entrySet返回值的iterator方法时返回的是EntryIterator在EntryIterator上调用next方法时最终实际调用到了HashIterator.advance()方法看下这个方法final void advance() { if (nextEntry ! null (nextEntry nextEntry.next) ! null) return; while (nextTableIndex 0) { if ( (nextEntry currentTable[nextTableIndex--]) ! null) return; } while (nextSegmentIndex 0) { SegmentK,V seg segments[nextSegmentIndex--]; if (seg.count ! 0) { currentTable seg.table; for (int j currentTable.length - 1; j 0; --j) { if ( (nextEntry currentTable[j]) ! null) { nextTableIndex j - 1; return; } } } } }这个方法在遍历底层数组。在遍历过程中如果已经遍历的数组上的内容变化了迭代器不会抛出ConcurrentModificationException异常。如果未遍历的数组上的内容发生了变化则有可能反映到迭代过程中。这就是ConcurrentHashMap迭代器弱一致的表现。总结ConcurrentHashMap的弱一致性主要是为了提升效率是一致性与效率之间的一种权衡。要成为强一致性就得到处使用锁甚至是全局锁这就与Hashtable和同步的HashMap一样了。