You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
//第三次进行上锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 前后2次的统计结果一致,可以返回
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
一、 ConcurrentHashMap的数据结构(JDK7)。
二、segment中独占锁的加锁逻辑
put操作加锁
reomve操作加锁
如果 tryLock() 不能能加锁成功则进行自旋,scanAndLockForPut和scanAndLock有点区别但是逻辑差不多。
1.有限重试次数,多核心CPU的话是64次,单核1次,超过次数则阻塞等待获取锁。
2.获取锁之前和获取到锁期间头节点不能发生改变,否则需要从头开始重试。
先说下结论,ConcurrentHashMap get方法不存在线程安全问题,他的线程安全是由CAS 和 "volatile"保证的:
我们整理下,要保证get不发生线程安全问题需要保证什么?
我们先看第1点时怎么保证的,我们都知道java中有个volatile用了保证变量在多线程下的可见性,volatile可以保证可见性,但是不能保证线程安全,如果当前赋值语句依赖当前值时是线程不安全的,比如a +=1 这种操作就是不安全的,显然在ConcurrentHashMap的并不需要这种操作,只存在简单的引用赋值操作。
但是需要注意的是一点,volatile修饰引用型变量时,只能保证当前引用的可见性,对于引用对象的内部变量仍然是无法保证可见性的,这就是为什么在对segments[] 数组和table[] 数组的的操作需要借助Unsafe类,而不是直接segments[i] = new Segment(...);
由前面的分析看,volatile/Unsafe.getObjectVolatile/Unsafe.putOrderedOject保证链当get操作晚与put操作时是可以获取到刚插入的节点(作为一个新头节点连接到旧节点并更新table),对与一个早于put操作的get操作一个情况就是新插入的元素表头,但是get操作已经获取到了旧表头,所以并不影响get操作进行链表的遍历查找。
我们在看进行remove时是否会影响entries的遍历,从源码中看,HashEntry中的next成员是被volatile修饰的,这就保证了get可以安全得遍历到未节点。
三、size的实现逻辑
假如ConcurrentHashMap采用HashMap维护一个全局的size来变量统计大小,那么为了线程安全,也必定得改用原子类AtomicLong或者全局加锁。这显然与分段锁的设计背离。那么有没有一种比较折衷的办法呢?
ConcurrentHashMap中将size的统计拆分到各个segment取去护,每次执行size的时候将每个segment的count加起来,最终得到的结果就是map的大小。这个看似乎很合理,但是如果在进行统计的过程中有一个segment发生put或者remove操作呢,这样得到的结果就是错误的,显然我们可以在统计前先将每个segment给锁起来,再sum,得到的结果肯定是正确的。
存在一种情况就是你的程序中并发很少,出现并发更新的情况很少,这个时候你执行size的时候将所有的segment加锁和不加锁的情况可能得到的结果是一样的,因为这个时候没有其他线程进行修改。似乎我们可以乐观地考虑一下大部分情况下是不需要进行锁操作的。
Doug Lea采用类一种跟JDK集合类中大多数存在的fail-safe错误检查机制,对在每个segment中于更新操作维护一个modCount来记录更新的次数,统计前和统计后的modCount是一样的说明没有发生变化,当前的统计结果有效。ConcurrentHashMap的size方法的实现逻辑如下:
四、Unsafe.getObjectVolatile/Unsafe.putOrderedOject对偏移量的计算问题
ConcurrentHashMap中使用量Unsafe类来对segment数组和table数组进行数组填充和取值操作,其中对位置i的内存偏移计算用了位运算来代替乘法运算
上面的计算方法是利用乘法来计算的,但是乘法的计算还是比较慢的,如果能用位运算更佳。由于jvm给对象分配内存的时候会进行内存对对齐,也就是说indexScale其实会是一个2的n次方的数。一个整数i乘以一个2的n次方可以转化成 i<<n;
所以你会看到ConcurrentHashMap中有这样的代码
测试用例-ConcurrentHashMap中利用Unsafe进行数组操作的测试用例;
The text was updated successfully, but these errors were encountered: