ConcurrentHashMap 源码分析 (二)

本文参考:http://www.javaeye.com/topic/344876及后续评论。

接上文,CurrentHashMap的containsValue 方法在第一层for循环的时候读取了count,但是并没用到这个变量, int c = segments[i].count;这是因为segment[i].count是对volatile变量的访问,接下来segments[i].modCount才能得到几乎最新的值。写volatile变量和它之前的读写操作是不能reorder的,读volatile变量和它之后的读写操作也是不能reorder的。
修改modCount发生在修改count之前,由于count是volatile变量,修改modCount不能和写count的操作reorder,读取count和它之后的操作,比如读取modCount,不能reorder。有了这两个不能reorder才能保证读取了count之后,能读到线程在写count之前的写入的modCount值,这个modCount值是几乎最新的。
如果在读modCount之前不读count,读modCount甚至可能会reorder到写modCount之前。

用reorder解释总是太复杂了,不如用happens-before来得简洁。当一个线程I对count的读时,它读到的值必定是另一个线程,假设是线程II,最近对count的写。这个两个操作存在happens-before关系,即线程II对count的写happens-before线程I对count的读,记作:II:W(count) < I:R(count)。单线程的happens-before规则,又有II:W(modCount) < II:(count)(查看源代码会发现在写count之前必定有写modCount),以及 I:R(count) < I:R(modCount),根据传递规则有,II:(modCount) < I:(modCount),这就是说线程I至少能够读取到线程II写count之前的modCount值。这里有一篇关于happens-before的文章,http://www.javaeye.com/topic/260515。 接下来学习下。

还有就是segment中的get方法,get操作不需要锁。第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count变量,通过这种机制保证get操作能够得到几乎最新的结构更新。对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。接下来就是对hash链进行遍历找到要获取的结点,如果没有找到,直接访回null。对hash链进行遍历不需要加锁的原因在于链指针next是final的。但是头指针却不是final的,这是通过getFirst(hash)方法返回,也就是存在table数组中的值。这使得getFirst(hash)可能返回过时的头结点,例如,当执行get方法时,刚执行完getFirst(hash)之后,另一个线程执行了删除操作并更新头结点,这就导致get方法中返回的头结点不是最新的。这是可以允许,通过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据,只有采用完全的同步。但是get能够看到调用get方法之前所有已经完成的put(因为如果发生结构性改变它会写count),虽然可能看不到正在进行的put操作(还没有写count)。这是一个很小的时间窗口,这不可避免,因为get没有同步。这并不是说get不安全,即使get操作时,其它线程在put,get也能看到一致的hash链(与HashMap相比较)。在HashMap中,假设put方法是同步的,get没有同步,get有可能看到实际上从不存在hash链。

get方法总是返回最近put过的数据,这句话说得很别扭,却是很重要的。假设其它线程先后调用了put("a", 1),put("a", 2),put("a", 3),这里能够使用“先后”,是因为put方法是同步的,put方法是排序的,每一个时刻只能够发生一个put方法。接着一个线程调用get("a")方法,并且另一个线程正在调用put("a", 4)方法,那么get("a")会返回3或者4,完全取决于("a")访问结点的值(读value)和put("a", 4)写结点的值(写value)哪个先发生(读volatile变量和写volatile总有个先后顺序,读写普通变量就不一定了,它们可能同时发生),这时put不会发生结构性更新。如果在调用get("a")时,另一个线程正在调用remove("a"),get方法要么返回3,要么返回null,这取决于get方法中getFirst(hash)返回的到底是remove("a")之前还是之后的头结点,而绝不会返回1, 2或任何其它的值。

所以CurrentHashMap并不能保证100%的一致性,只能在性能和一致性之间达到一个比较好的平衡,如果要100%的一致估计还是要用Collections.synchronizedMap(Map map)

你可能感兴趣的:(ConcurrentHashMap 源码分析 (二))