一、ConcurrentHashMap源码解析(jdk1.7)
1.1 主要成员变量
//默认容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//Segment数组
final Segment<K,V>[] segments;
1.2 Segment段
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//链表数组
transient volatile HashEntry<K,V>[] table;
//Segment中元素的数量
transient int count;
//修改的次数
transient int modCount;
//阈值,段中元素的数量超过这个值就会对Segment进行扩容
transient int threshold;
//段的负载因子,其值等同于ConcurrentHashMap的负载因子
final float loadFactor;
...
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
由此可见,ConcurrentHashMap是Segment对象数组,每个Segment都是一个hashmap,而这个Segment继承了可重入锁,ConcurrentHashMap采用了分段锁的思想来实现,对一个Segment的加锁不影响其他的Segment读写,HashEntry和value都用volatile修饰保证了内存可见性,读的时候都是最新的值,所以读不用枷锁,所以能进行高并发读写。
1.3 put操作
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
//根据key的hash确定是哪个Segment,再确定在Segment中的位置
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//先加锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;//确定索引
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {//数组中当前位置存在key
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;//key相等则直接覆盖
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {//数组中当前位置不存在key
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);//把node放在数组中
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {//解锁
unlock();
}
return oldValue;
}
大概流程:先根据key的hash定位到Segment,然后对找到的Segment加锁而不是对所有Segment加锁,再在Segment中找到具体的位置,这段逻辑就和HashMap的差不多了,最后释放锁。
1.4 get操作
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//先找到Segment,然后遍历Segment里的数组
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get操作没有加锁。
1.5 size方法
public int size() {
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 (;;) {
//如果重试次数大于2,那么对每一个segments加锁
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;
}
}
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;
}
大致流程:
- 在一个死循环里遍历所有的segment。
- 把每一个segment的size加起来得到sum。
- 判断这次循环的sum和上次循环的sum是否相等,如果相等则说明在此期间size没有改变,直接退出循环;如果不相等则说明其他线程在统计的时候改变了size,那么继续循环重试,当重试次数超过了阈值则对所有segment加锁,这样就保证了统计的时候size不会再改变了。
- 如果加锁了则进行解锁,返回size。
从这里可以看到乐观锁的思想,就先乐观地假设统计的过程中size不会被其他线程修改,如果真发生了修改那么就多尝试几次,实在不行再加锁。
二、ConcurrentHashMap常见面试问题
2.1 ConcurrentHashMap的数据结构?
jdk1.7:
本质上,ConcurrentHashMap就是个存储Segment对象的数组,而这个Segment又是一个HashMap的结构,Segment继承了可重入锁,从而使得Segment对象能充当锁的角色,这样,每个Segment守护ConcurrentHashMap的若干个桶,其中每个桶又是由若干个HashEntry对象链接起来的链表。通过使用段(Segment)将ConcurrentHashMap分成多个部分,这样每一个部分都有一把锁,修改一个部分不影响其他部分同时修改,就是允许多个部分同时修改,这就是分段锁的核心思想。
jdk1.8:
取消了分段锁,结构上和HashMap1.8的结构相似,都是数组+链表+红黑树的结构,采用CAS操作和synchronized来保证并发安全。
2.2 jdk1.7和jdk1.8的put实现有什么不同?
jdk1.8put源码:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//key和value都不能是空
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // 当数组的下标位置为空时,进行不加锁的CAS操作赋值
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
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;//如果key相等,则直接覆盖掉value
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, null);
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
如果定位到数组上的node为空,则用CAS操作进行写入数据,如果有数据则用synchronized锁住链表的头节点进行写入。
2.3 ConcurrentHashMap和HashMap有什么区别?
- ConcurrentHashMap线程安全,HashMap线程不安全。
- ConcurrentHashMap不允许存储null键和null值,HashMap允许存储null键和null值。
2.4 ConcurrentHashMap的key和value为什么不能为null?
因为作者Doug Lea不喜欢null。无法分辨是key没找到的null还是有key值为null。
三、总结
在jdk1.8中ConcurrentHashMap放弃了Segment分段锁,采用CAS和synchronized来保证线程安全,这是一种乐观锁的思想,细化了锁的粒度。