java ConcurrentHashMap中的一点点迷惑
顺便再温习一下ConcurrentHashMap源码,读到isEmpty()方法时,对其中的:
?
public boolean isEmpty() { final Segment<K,V>[] segments = this.segments; /* * We keep track of per-segment modCounts to avoid ABA * problems in which an element in one segment was added and * in another removed during traversal, in which case the * table was never actually empty at any point. Note the * similar use of modCounts in the size() and containsValue() * methods, which are the only other methods also susceptible * to ABA problems. */ int[] mc = new int[segments.length]; int mcsum = 0; for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0) return false; else mcsum += mc[i] = segments[i].modCount; } // If mcsum happens to be zero, then we know we got a snapshot // before any modifications at all were made. This is // probably common enough to bother tracking. if (mcsum != 0) { for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0 || mc[i] != segments[i].modCount) return false; } } return true; }?
? ?产生了一些问题(其实叫疑问更好,有可能不是问题):
? ?1、从Segment内部类中可以看到modCount属性是非volatile的,也就是如果对其进行了修改,在isEmpty()方法中对其值的访问不一定能访问到最新值。
? ?2、使用mcsum值并不能完全解决ABA问题情况,因为mcsum += mc[i] = segments[i].modCount;这句话只对for循环中还没有循环到的变量可能会读取到最新值,但是对已经循环过的segment,如果其间有值加入,这时的isEmpty()语意是无法保证的,因为mcsum值已经累加过了以前的值,并无法感知最新值。
例如:此时for循环刚好循环到segments[3],此时已经累加了前面segments[0],segments[1],segments[2]的modCount,而此时刚好segments[0]被插入了一个新值,此时segments[0].modCount值是变化了的,而此时已经无法再修改mcsum的值了。因为这些变化都是发生在该遍历期间的,理应算做在计算期间的结果,如果该计算结果为空,而在计算的期间是有值插入的,则该计算期间的非空应该作为返回结果:非空。。
? 3、再次确认非空时:
? ? ? ?if (mcsum != 0) {
for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0 || mc[i] != segments[i].modCount) return false; } }? ?这个逻辑再次无法保证,因为没有使用同步,再次无法保证读取到的modCount是最新值(据自己了解的并发知识,modCount有可能会被线程缓存,而无法得到最新值)。
?
? ?带着这众多的疑问,我找遍了网络都没有找到合理的说法。。。。
?
? ?最后在一本书《Java并发编程实践》中找到了一个比较合理的说法
?
?
ConcurrentHashMap尽管有这么多改进,我们仍然有一些需要权衡的地方。那些对整个Map进行操作的方法,如size和isEmpty,它们的语义在反映容器并发特性上已经被轻微的弱化了。因为size的结果相对于在计算的时候可能已经过期,它仅仅是一个估算值,所以允许size返回一个近似值而不是一个精确值。这在一开始让人有些困扰,不过事实上像size和isEmpty这样的方法在并发环境下几乎没有什么用处,因为它们的目标是运动的。所以对这些操作的需求被弱化了。相反,应该保证对最重要的操作进行性能优化,首先就包括get、put,containsKey和remove。
?
?
? 茅塞顿开,这就是为什么可以对isEmpty可以采取非加锁方式了,同时如果某个方法在调用isEmpty时进行对Segment加锁仍能达到putIfAbsent的效果!
?
?