前言
ConcurrentHashMap是Java中的高并发容器,具有线程安全、高并发、高性能等特点。在本文中,我们将通过分析ConcurrentHashMap的数据结构、锁机制、LongAdder和computeIfAbsent方法的实现原理,来深入了解这一容器类。
ConcurrentHashMap的数据结构
ConcurrentHashMap是基于哈希表实现的,其底层数据结构是由多个Segment组成的Segment数组,每个Segment内部都是一个与HashMap类似的链表结构(JDK1.7),或者是一个数组加链表结构(JDK1.8)。对于一个键值对,首先会根据其键值的哈希值定位到一个Segment,然后在该Segment内部进行操作。
以下是ConcurrentHashMap的构造方法源码:
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0F) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// java8之前
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// Segments数组中存放Segment对象
Segment<K,V>[] segments = (Segment<K,V>[])new Segment[cap];
// 构造每个Segment对象
for (int i = 0; i < segments.length; ++i)
segments[i] = new Segment<K,V>(loadFactor);
this.segments = segments;
}
在源码中,我们可以看到ConcurrentHashMap的构造方法接收三个参数:initialCapacity表示初始容量大小,loadFactor表示负载因子,concurrencyLevel表示并发级别。其中,concurrencyLevel参数用来初始化ConcurrentHashMap的Segment数组大小,也就是ConcurrentHashMap内部开启的线程数量,因为每个Segment都会对应一个独立的线程。在初始化过程中,通过Segment数组进行ConcurrentHashMap的并发控制。
ConcurrentHashMap的锁机制
在ConcurrentHashMap中,对于每个Segment,使用重入锁(ReentrantLock)来实现线程安全。对于已经锁定的Segment,在插入或删除元素时,只有等待其它线程操作完成后才能进行,以此避免多个线程同时操作同一个Segment导致的并发问题。而对于未被锁定的Segment,可以并发地执行读取操作,不会出现并发问题。
以下是ConcurrentHashMap的Segment的构造方法和插入元素的方法源码:
static class Segment<K,V> extends ReentrantLock implements Serializable {
private transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
Segment(float lf) {
this.loadFactor = lf;
this.threshold = (int)(DEFAULT_CAPACITY * lf);
this.table = new HashEntry[DEFAULT_CAPACITY];
}
V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 插入元素时先获取锁
lock();
// ...
}
// ...
}
在上述代码中,我们可以看到,在Segment的构造方法中通过继承ReentrantLock实现了对Segment的重入锁机制。而在插入元素时,则需要首先获取Segment的锁,保证只有一个线程在进行插入操作。
ConcurrentHashMap的并发度与性能
ConcurrentHashMap的并发度和性能表现非常优秀,主要得益于其对读写操作的优化。由于ConcurrentHashMap在初始化时会指定内部开启的线程数量,因此可以避免像同步容器那样在进行读取、插入或删除操作时,多个线程被阻塞的问题,大大提升了并发性能。
另外,ConcurrentHashMap中的元素插入和查询操作都使用了volatile关键字,保证对元素的修改和读取操作可见性,进一步提高了并发性能。
LongAdder的实现原理
LongAdder是ConcurrentHashMap中的一个类,用于对值进行累加计数。与AtomicLong不同,LongAdder分散了计数,因此在高并发环境下比AtomicLong要快。具体来说,LongAdder使用了分段锁机制,将计数值分别记录在各自的cell中,每次更新时只需要对对应的cell进行加法计算,而不需要对所有的计数值进行加法计算。
以下是LongAdder的源码:
public class LongAdder {
private static final Unsafe UNSAFE = Unsafe.getUnsafe();
private static final long BASE;
private static final int SHIFT;
private static final int CELLSBUSY;
private static final long[] PROBE;
volatile transient Cell[] cells;
public LongAdder() {
this.cells = null;
}
public void add(long x) {
Cell[] as;
long b, v;
int m;
Cell a;
if ((as = this.cells) != null || !this.casBase(b = this.base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) {
this.longAccumulate(x, (LongBinaryOperator)null, uncontended);
}
}
}
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current();
h = getProbe();
this.setProbe(h == 0 ? 1 : h);
wasUncontended = true;
}
boolean collide = false;
while(true) {
Cell[] as;
Cell a;
int n;
if ((as = this.cells) != null && (n = as.length) > 0) {
if ((a = as[h & n - 1]) == null) {
if (this.busy == 0) {
Cell r = new Cell(x);
if (this.busy == 0 && this.casBusy()) {
boolean created = false;
try {
Cell[] rs;
int m;
int j;
if ((rs = this.cells) != null && (m = rs.length) > 0 && rs[j = n - 1 & h] == null) {
rs[j] = r;
created = true;
}
} finally {
this.busy = 0;
}
if (created) {
break;
}
continue;
}
}
collide = false;
} else if (!wasUncontended) {
wasUncontended = true;
} else {
long v = a.value;
if (a.cas(v, fn == null ? v + x : fn.applyAsLong(v, x))) {
break;
}
if (n >= CELLSBUSY || this.cells != as) {
collide = false;
} else if (!collide) {
collide = true;
} else if (this.busy == 0 && this.casBusy()) {
try {
if (this.cells == as) {
Cell[] rs = new Cell[n << 1];
for(int i = 0; i < n; ++i) {
rs[i] = as[i];
}
this.cells = rs;
}
} finally {
this.busy = 0;
}
collide = false;
continue;
}
}
h ^= h << 13;
h ^= h >>> 17;
h ^= h << 5;
this.setProbe(h);
continue;
}
if (this.busy == 0 && this.cells == as && this.casBusy()) {
boolean init = false;
try {
if (this.cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
this.cells = rs;
init = true;
}
} finally {
this.busy = 0;
}
if (init) {
break;
}
continue;
}
if (this.casBase(b, fn == null ? b + x : fn.applyAsLong(b, x))) {
break;
}
}
}
// ...
}
上述代码中,我们可以看到LongAdder实现的核心是通过Cell数组来记录各自的计数值,并使用分段锁机制实现并发控制,从而提高了并发性能。
computeIfAbsent方法的实现原理
computeIfAbsent是ConcurrentHashMap中的一个方法,用于在ConcurrentHashMap中的键key不存在时,通过计算方式新增一个键值对。首先,computeIfAbsent方法会根据key的hash值得到其在ConcurrentHashMap中对应的Segment。然后,在该Segment内部进行操作:首先会对Segment进行加锁,然后判断该key是否存在,如果不存在,则通过Function计算出对应的value值,并插入到该Segment中。
以下是computeIfAbsent方法的源码:
public V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
// 计算key的哈希值
int h = spread(key.hashCode());
// 插入元素时先获取锁
Segment<K,V> segment = segmentForHash(h);
return segment.compute(key, h, mappingFunction, false);
}
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (key == null || remappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
Segment<K,V> segment = segmentForHash(h);
return segment.compute(key, h,
(k, oldValue) -> {
V newValue = remappingFunction.apply(k, oldValue);
return (newValue == null) ?
null :
checkType(newValue);
},
true);
}
从上述代码中,我们可以看到,在Segment的compute方法中,如果key不存在于Segment中,则调用mappingFunction计算出对应的value,并插入到Segment中。compute方法使用了CAS无锁技术实现了线程安全。
总结
ConcurrentHashMap是一个高并发容器,其源码实现比较复杂,本文从数据结构、锁机制、并发度和性能、LongAdder和computeIfAbsent等方面进行分析。无论是在学习Java并发编程,还是在实际应用中操作高并发容器,对于ConcurrentHashMap的深入了解都有重要的意义。