ConcurrentHashMap也是并发环境中常见的Map,如果在高并发中没有排序等特别的需要,我们可以优先选择ConcurrenHashMap存储key-value键值对。
ConcurrentHashMap一般有两个版本的实现,jdk7(包括7)之前是Segment数组+Hash桶的数据结构,jdk8(包括8)之后是synchronized+cas+红黑树的数据结构。
下面我们先来比较下各种常见Map区别,然后研究两个版本的ConcurrentHashMap。
一、各种常见Map的比较
- HashMap:最基础的Map结构,在非并发场景下,优先使用。缺点是非线程安全的。
- HashTable:线程安全,内部方法都使用了synchronized加锁,操作比较耗性能。
- Collections.synchronizedMap():线程安全,与HashTable类型,都是使用synchronized对每个方法做了加锁,比较耗性能。
- ConcurrentHashMap:线程安全,操作效率较高。
二、JDK7版本的ConcurrentHashMap
1.Segment对象源码分析
JDK7以及之前版本的ConcurrentHashMap采用了分段锁Segment数组+Hash桶的数据结构,相当于是做了两次Hash。第一次Hash拿到分段锁,默认是16个Segment,每个Segment继承了ReentrantLock可以加锁,里面又维护了一个跟HashMap类似的Hash桶来保存key-value数据。结构图大致如下:
其核心在Segment类中,我们通过下面源码来分析。
可以看到Segment继承了重入锁ReentrantLock,所以它可以调用tryLock方法加锁。另外从其内部的代码结构可以看到,它也可以当成是一个HashMap。可以从put方法中看到,它就是调用了tryLock加锁,然后操作内部数据。可以理解是先加锁然后操作HashMap。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
// Segment的元素
transient volatile HashEntry<K,V>[] table;
// Segment中保存对象个数
transient int count;
/** 扩容的阈值 */
transient int threshold;
/** 负载因子 */
final float loadFactor;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 这里省略部分代码
.......
} finally {
unlock();
}
return oldValue;
}
2.put方法分析
put方法先是通过hash找到当前key所对应的Segment,然后调用这个Segment对象的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;
/** 内部维护了一个Segment的数组,默认数组大小为16,
ConcurrentHashMap初始化的时候会把下标为0的Segment放到数组中,
后面根据j的值从Segment数组中找到对应下标的Segment,
如果找不到就创建一个Segment,并放到数组中 */
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
3.size方法
size方法会遍历每个segment,并对其加锁,然后把每个segment保存的对象数量相加,最后解锁。
4.get方法
get方法不做加锁,首先拿到Segment,然后再去Segment里面的HashEntry数组里找到value
三、JDK8版本的ConcurrentHashMap
JDK8版本的ConcurrentHashMap比较复杂,这里就根据put方法的实现来大概来分析下其结构
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// key对应的hash值
int hash = spread(key.hashCode());
int binCount = 0;
// table是哈希桶,跟HashMap类似
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 找到hash对应的桶
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶中之前没值,把当前值插入成功后可直接退出轮询
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 对操作的节点加锁,这里直接使用了synchronized,可以猜测jdk8对synchronized做了很多优化,可以直接使用在并发类了
synchronized (f) {
// 加锁之后就是构建链表或者红黑树,如果哈希桶中一个下标位置对应的节点数大于等于8就构建红黑树
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, 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) {
// 链表数量大于等于8构建红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}