concurrenthashmap sizectl,concurrenthashmap默认大小
00-1010 concurrent hashmap的大小方法原理。先说并发hashmap的size方法的思路。我们首先会想到以下两种方法。
00-1010同上。这也是在同一次面试中被别人问到的。只记得看过,在concurrenthashmap里算了很多次。当时我说我再数两遍比较一下,人家接着问为什么。我太傻了。这两个统计中间不是明显有新变化,会导致统计不准吗?我当时不知道该说什么。我以为他有新东西,就说不知道。面试的时候,很多问题其实静下心来想一想,就可以更进一步了。有时候,你害怕他走得更远之后,下面的坑会被挖得更大。
目录
代码就不贴了。只说原理。
众所周知,concurrenthashmap中有很多歌曲片段。首先,遍历这些段,并将每个段的计数加起来作为整个concurrenthashMap的大小。如果没有并发性,这是正常的,但它是多线程的。如果统计后前脚变了,是不准确的。源代码中引入了modCount和两次比较来实现大小的确认。具体流程是:
1.进行第一遍遍历segments数组
将每一段的计数加起来作为总数,将每一段的模计数加起来求和,作为判断结果是否被修改的依据。
这里有必要提一下modCount。当段中有任何操作时,这是一个增量操作,它表示影响段中元素数量的操作数量。这个值只增不减!重要的是只增加或减少,这样就不会有一个导致modcount 1的segment 1,和另一个segment-1,即modcount-1,这样当所有的统计数据都可用时modcount不会改变。
2.size操作就是遍历了两次所有的Segments
记录每个分段的modCount值,然后比较两个mod count。如果相同,说明期间没有写操作,返回原来的遍历结果。如果它们不相同,该过程将再次重复。如果它们不相同,则需要逐个锁定和遍历所有段。
3.如果经判断发现两次统计出的modCount并不一致
那么,如上所述,需要重新启用锁定所有segements的方式来获取和计数。这样,在此期间,每个段都被锁定,不能进行其他操作。算出来的数自然是准确的。
之所以要先判断不加锁,原因很明显,就是不想因为大小操作而获取那么多锁,因为获取锁不仅占用资源,还会影响其他线程对ConcurrentHash的使用,以及并发条件下程序执行的效率。小心使用锁!
大概就是这个原理吧。具体代码可以看源码,源码1.7和1.8有区别。有时间再贴上来对比一下。
concurrenthashmap的size方法原理
ConcurrentHashMap通过分段锁控制整个Map的安全性和并发性。那么在寻求大小时,ConcurrentHashMap如何平衡性能和安全性呢?
下面具体说一下这个size方法
1.获取所有的Segment锁。
这种方法是可行的,但是会导致较差的并发性能,因为如果您获取了所有的锁,其他线程将无法在HashMap上执行任何操作。
2.逐个地获取Segment。
这种方法也有问题。有可能后面得到下一段的元素个数时,上一段的元素个数很可能已经发生了变化,所以最后累加到最后时数据可能是错误的。
那么ConcurrentHashMap采取什么措施呢?源代码如下:
java1.7以前的源码:
因为之前累计的计数在累计计数的过程中发生变化的概率非常小,所以ConcurrentHashMap首先尝试通过不锁定两次来统计每个段的大小。如果在计数的过程中该段的计数发生变化,则将其锁定以计数该段的计数。
java1.7以及1,7以后的源码:
size的核心是sumCount函数。
最终long sum count(){ count cell[]as=count cells
; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }核心逻辑:当 counterCells 不是 null,就遍历元素,并和 baseCount 累加。
查看两个属性:baseCount 和 counterCells。
先看 baseCount
private transient volatile long baseCount;
baseCount是一个 volatile 的变量,在 addCount 方法中会使用它,而 addCount 方法在 put 结束后会调用。在 addCount 方法中,会对这个变量做 CAS 加法。
private final void addCount(long x, int check) { CounterCell[] as; long b, s; if ((as = counterCells) != null !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; if (as == null (m = as.length - 1) < 0 (a = as[ThreadLocalRandom.getProbe() & m]) == null !(uncontended = U.compareAndSetLong(a, CELLVALUE, v = a.value, v + x))) { fullAddCount(x, uncontended); return; } if (check <= 1) return; s = sumCount(); }
但是如果并发导致 CAS 失败了,怎么办呢?使用 counterCells。
如果上面 CAS 失败了,在 fullAddCount 方法中,会继续死循环操作,直到成功。
最后,再来看一下counterCells这个类。
@jdk.internal.vm.annotation.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; } }
上述源码中的注释是为了避免伪共享(false sharing)。
先引用个伪共享的解释: 缓存系统中是以缓存行(cache line)为单位存储的。
缓存行是2的整数幂个连续字节, 一般为32-256个字节。最常见的缓存行大小是64个字节。
当多线程修改互相独立的变量时, 如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持盛行IT。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。