注:本文根据网络和部分书籍整理基于JDK1.7书写,如有雷同敬请谅解 欢迎指正文中的错误之处。
ConcurrentHashMap是HashMap的一个线程安全的、支持高效并发的版本。在多线程并发场景下HashTable和由同步包装器包装的HashMap,都是通过使用一个全局的锁来同步不同线程间的并发访问,会带来不可忽视的性能问题。
ConcurrentHashMap采用分段锁的设计Segment + HashEntry的方式进行实现,采用table数组+单向链表的数据结构,结构如下:
initialCapacity:初始容量(默认16),这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
concurrencyLevel:并发数(Segment 数,默认16)。ConcurrentHashMap采用了分段锁的设计,分段锁个数即Segment[]的数组长度,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争,所以当它们分别操作不同的 Segment时最多可以同时支持16个线程并发。
注:用户可以在构造函数中初始设置为其他值,当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(如用户设置并发度为17,实际并发度则为32),但是一旦初始化以后,它是不可以扩容的。
不能超过规定的最大值:final int MAX_SEGMENTS = 1 << 16
segments:Segment 通过继承 ReentrantLock 来进行加锁,每次需要加锁的操作锁住的是一个 segment,每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶,这样保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
注:JDK7中除了第一个Segment之外,剩余的Segment采用的是延迟初始化的机制:每次put之前都需要检查key对应的Segment是否为null,如果是则调用ensureSegment()以确保对应的Segment被创建。Segment[i] 的默认大小为 2,负载因子是 0.75
table: 由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。
count:计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。
注:之所以在每个 Segment 对象中包含一个计数器,而不在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。需要更新计数器时,不用锁定整个 ConcurrentHashMap。
loadFactor:负载因子(默认0.75),之前我们说了,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的。
1、判断value是否为null,如果为null,直接抛出异常。注:不允许key或者value为null
2、通过哈希算法定位到Segment(key通过一次hash运算得到一个hash值,将得到hash值向右按位移动segmentShift位,然后再与segmentMask做&运算得到segment的索引j)。
3、使用Unsafe的方式从Segment数组中获取该索引对应的Segment对象
4、向这个Segment对象中put值
注:对共享变量进行写入操作为了线程安全,在操作共享变量时必须得加锁,持有段锁(锁定整个segment)的情况下执行的。修改数据是不能并发进行的
判断该值的插入是否会导致该 segment 的元素个数超过阈值,以确保容量不足时能够rehash扩容,再插值。
注:rehash 扩容 segment 数组不能扩容,扩容的是 segment 数组某个位置内部的数组 HashEntry[] 扩容为原来的 2 倍。先进行扩容,再插值
查找是否存在同样一个key的结点,存在直接替换这个结点的值。否则创建一个新的结点并添加到hash链的头部,修改modCount和count的值,修改count的值一定要放在最后一步。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
1、计算 hash 值,找到 segment 数组中的具体位置,使用Unsafe获取对应的Segment
2、根据 hash 找到数组中具体的位置
3、从链表头开始遍历整个链表(因为Hash可能会有碰撞,所以用一个链表保存),如果找到对应的key,则返回对应的value值,否则返回null。
注:get操作不需要锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* @throws NullPointerException if the specified key is null
*/
public V get(Object key) {
Segment s; // manually integrate access methods to reduce overhead
HashEntry[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry e = (HashEntry) 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;
}
size操作需要遍历所有的Segment才能算出整个Map的大小。先采用不加锁的方式,循环所有的Segment(通过Unsafe的getObjectVolatile()以保证原子读语义)连续计算元素的个数,最多计算3次:
1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;
注:在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。
/**
* Returns the number of key-value mappings in this map. If the
* map contains more than Integer.MAX_VALUE elements, returns
* Integer.MAX_VALUE.
*
* @return the number of key-value mappings in this map
*/
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[] 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 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;
}