JDK1.7中的ConcurrentHashMap
-
JDK1.7中的ConcurrentHashMap采用了分段锁的形式, 每一段为一个Segment类, 它内部类似HashMap的结构, 内部有一个Entry数组, 数组的每个元素是一个链表。同时Segment类继承自
ReentrantLock
。 -
底层结构大致如图(省去红黑树的逻辑)
- 在HashEntry中采用了volatile来修饰了HashEntry的 `当前值` 和 `next元素`。所以 `get` 方法在获取数据的时候是不需要加锁的, 这样就大大的提升了执行效率。
- 在执行 `put()` 方法的时候会先尝试获取锁(`tryLock()`), 如果获取锁失败, 说明存在竞争, 那么将通过`scanAndLockForPut()`方法执行自旋, 当自旋次数达到`MAX_SCAN_RETRIES`时会执行阻塞锁, 直到获取锁成功。
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 首先尝试获取锁, 获取失败则执行自旋, 自旋次数超过最大次数后改为阻塞锁, 直到获取锁成功;
HashEntry<K,V> node = tryLock() ? null :
// CAS失败, 自旋 + lock实现逻辑;
scanAndLockForPut(key, hash, value);
// CAS成功, 走下边流程;
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 计算哈希码;
int index = (tab.length - 1) & hash;
// 拿到 segment 首节点;
HashEntry<K,V> first = entryAt(tab, index);
// 遍历链表;
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 如果 key 存在, 则进行 value 替代;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
// key 不存在, 进行队列节点插入;
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
// 同步情况下修改 modCount;
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 你懂的;
unlock();
}
return oldValue;
}
JDK1.8后的ConcurrentHashMap
- 在JDK1.8中, 放弃了Segment这种分段锁的形式, 而是采用了
CAS + Synchronized
的方式来保证并发操作, 采用了和HashMap一样的底层数据结构, 直接用数组加链表, 在链表长度大于8的时候为了提高查询效率会将链表转为红黑树(链表定位数据的时间复杂度为O(N), 红黑树定位数据的时间复杂度为O(logN))。 - 在代码上也和JDK1.8的HashMap很像, 也是将原先的HashEntry改为了Node类, 但还是使用了volatile 修饰了
当前值
和next的值
。从而保证了在获取数据时候的高效。 - JDK1.8中的ConcurrentHashMap在执行
put()
方法的时候还是有些复杂的, 主要是为了保证线程安全才做了一系列的措施。
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 防止并发环境下歧义, 禁止空的键或值;
if (key == null || value == null) throw new NullPointerException();
// hash计算;
int hash = spread(key.hashCode());
// binCount 表示一个桶中的节点数量, 用于判断是否需要 链表 <-> 红黑树的转换;
int binCount = 0;
// 遍历所有的桶;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// 如果桶数组没有初始化, 则初始化;
if (tab == null || (n = tab.length) == 0)
// 初始化后重回遍历逻辑;
tab = initTable();
// 如果哈希码对应位无数据;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果CAS处理成功, 跳出循环;
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
// CAS失败, 说明存在竞争, 使用synchronized加锁;
// 当前hash码 == moved(-1) 时候, 需要扩容;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
// 上述不成立时候, 加锁处理;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 链表 <-> 红黑树;
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 统计, 走另外统计逻辑, 及时释放桶;
addCount(1L, binCount);
return null;
}
- 第一步通过key进行hash;
- 第二步判断是否需要初始化数据结构;
- 第三步根据 key 定位到当前 `Node`, 如果当前位置为空, 则可以写入数据, 利用 `CAS` 机制尝试写入数据, 如果写入失败, 说明存在竞争, 将会通过自旋来保证成功;
- 第四步如果当前的 哈希码 等于 `MOVED` 则需要进行扩容(扩容时也使用了`CAS`来保证了线程安全)。
- 第五步如果上面四步都不满足, 那么则通过 `synchronized` 阻塞锁将数据写入;
- 第六步如果数据量大于 `TREEIFY_THRESHOLD` 时需要转换成红黑树(默认为8);
- JDK1.8的ConcurrentHashMap的
get()
方法就还是比较简单:- 根据
key
的 哈希码 寻址到具体的桶上; - 如果是红黑树则按照红黑树的方式去查找数据;
- 如果是链表就按照遍历链表的方式去查找数据;
- 根据
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
- ConcurrentHashMap的
size()
方法-
JDK1.7中的ConcurrentHashMap的size方法, 计算size的时候会先不加锁获取一次数据长度,然后再获取一次, 最多三次。比较前后两次的值, 如果相同的话说明不存在竞争的编辑操作, 就直接把值返回就可以了。但是如果前后获取的值不一样, 那么会将每个Segment都加上锁, 然后计算ConcurrentHashMap的size值。
-
JDK1.8中的ConcurrentHashMap的
size()
方法的源码如下:
-
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
// 这个方法最大会返回int的最大值, 但是 ConcurrentHashMap 的长度有可能超过int的最大值。
- 在JDK1.8中增加了
mappingCount()
方法, 这个方法的返回值是long类型的, 所以JDK1.8以后更推荐用这个方法获取Map中数据的数量。
/**
* @return the number of mappings
* @since 1.8
*/
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}
- 无论是
size()
方法还是mappingCount()
方法,核心方法都是sumCount()
方法。
final long sumCount() {
CounterCell[] as = counterCells; 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;
}
baseCount
是一个volatile
变量, 那么我们来看在put()
方法执行时是如何使用baseCount的? 在put
方法的最后一段代码中会调用addCount()
方法, 而addCount()
方法的源码如下:
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
// 首先对baseCount进行CAS自增操作;
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 如果并发导致了baseCount的 CAS自增 失败了, 则使用 counterCells 进行 CAS;
CounterCell c; long v; int m;
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
// 如果 counterCells 的 CAS 也失败了, 那么则进入`fullAddCount()`方法, `fullAddCount()`方法中会进入死循环, 直到成功为止;
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
if (sc < 0) {
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount();
}
}
}
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
// 死循环, 直到成功;
for (;;) {
CounterCell[] cs; CounterCell c; int n; long v;
if ((cs = counterCells) != null && (n = cs.length) > 0) {
if ((c = cs[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))
break;
else if (counterCells != cs || n >= NCPU)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == cs) // Expand table unless stale
counterCells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = ThreadLocalRandom.advanceProbe(h);
}
else if (cellsBusy == 0 && counterCells == cs &&
U.compareAndSetInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == cs) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (U.compareAndSetLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
- 无论是JDK1.7还是JDK1.8中, ConcurrentHashMap的
size()
方法都是线程安全的, 都是准确的计算出实际的数量, 但是这个数据在并发场景下是随时都在变的。