ConcurrentHashMap
本来是想以JDK1.8为例的,只是后来发现源码用的是1.7的,所以这里就干脆以1.7进行说明吧,关于ConcurrentHashMap1.8中大体功能没变,主要是加了红黑树结构转换、CAS级别的锁控制。
1.7中ConcurrentHashMap包含一个Segment[]数组(默认大小16),Segment和HashHap的结构类似,内部包含一个HashEntry<K,V>数组,每个HashEntry元素都是一个链表结构,也就是说每个Segment都是一个数组+链表的结构。
同时Segment还继承自ReentrantLock,也就是说Segment本身还是一个可重入锁,这样的设计的好处在于,需要锁住该段数据时,直接用该HashEntry所在的Segment执行lock即可, 不需要额外的定义锁。
这种分析下,Segment在HashMap中充当锁的角色,同时管理HashEntry[],如扩容等操作。而Segment.HashEntry才是真正存储数据的地方。下面是1.7中的部分源码
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
...
final Segment<K,V>[] segments;
...
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//这段区域的元素个数
transient volatile int count;
// 记录该段在读取期间,表被其它线程更新的次数
transient int modCount;
//扩容阈值,当元素个数>=该值,扩容
transient int threshold;
//hashEntry数组
transient volatile HashEntry<K,V>[] table;
//加载因子,确定扩容阈值 = (int)(capacity * loadFactor)
final float loadFactor;
...
初始化
初始化有三个重要的参数
- initialCapacity:初始容量,这里是指整个map的容量,所以该值会平分到每个Segment。在initialCapacity/concurrencyLevel无法整除的情况下,初始化过程中也会保证最终每个segments中hashEntry的长度为大于该值的第一个2的n次方,比如在容量为31,segments.length为16的情况下,由于31/16=1无法整除,则每个segments中hashEntry的长度为2。不过在jdk1.7中只初始化了数组中的第一个元素
- loadFactor:加载因子
- concurrencyLevel:并发等级,由于锁在Segment上,所以这里并发等级即代表Segment[]大小,默认16。如果设为单数,在初始化过程中也会修改为大于该值的第一个2的n次方,比如给3则取4,给9则取16。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
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[0],内部包含的hashEntry[]长度=cap为2的n次方
Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
// 初始化segments[],ssize为2的n次方
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
static final class Segment<K,V> extends ReentrantLock implements Serializable {
...
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
...
从源码可以看出,初始化主要就是对segements[]数组及其元素进行初始化,最终保证了segemtns.length长度(并发等级)=2的n次方,segment.hashEntry链表长度=容量/并发等级=2的n次方。
put
put操作原理说起来比较简单,根据key的hash值得到其应存储的段,即segments[]下标,然后lock该segment(如果该段数量已达扩容阈值则进行rehash扩容操作)。同样根据hash值与HashEntry长度进行运算,再次得到其应存储在HashEntry中的下标,然后迭代链表,如果已存在相同key,则覆盖value,否则将值添加到链表头部。对应源码如下
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// 得到key的hash值
int hash = hash(key.hashCode());
int j = (hash >>> segmentShift) & segmentMask;
// 根据hash值计算得到segments[]下标,如果该位置下的segment尚未初始化则初始化。
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
...
static final class Segment<K,V> extends ReentrantLock implements Serializable {
...
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 获取独占锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 同样根据hash值计算得出,应存储在HashEntry[]中的下标位置
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
// 获取hashEntry[index]链表头部元素,通过.next迭代链表,如果已存在则覆盖值
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> 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<K,V>(hash, key, value, first);
int c = count + 1; // 该段元素个数+1,count变量为volatile
// 元素个数 > 扩容阈值,扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node); // 设置tab[index] = node
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
...
在put过程中存在一个并发问题,由于put会获取头部元素通过.next进行迭代,并最终将新元素置为链表的头部元素,当多个线程对一个segment执行put操作时,如何保证某个put操作对其它线程的可见性?即如何保证其它线程可以获取到最新put的元素(对应上方代码中的entryAt方法)。 虽然HashEntry[]数组本身是volatile的,但这只是保证扩容后,数组引用发生变化时被及时看到,并不能保证对元素的修改对其它线程也是及时可见的。不能表达数组元素是final、volatile的特性,这是Java语言的设计上的一个缺失。
所以在put方法中,获取链表头部entryAt(tab,index) 和 设为头部元素setEntryAt(tab,index)的方法中,用到了UNSAFE这个类,UNSAFE.putOrderedObject(...)是一个有序或者有延迟的putObjectVolatile方法,即它最终会强制该操作更新到主存,但并一定立即强制,但是Java的happend-before原则会保证其一定会在后续其它线程get之前更新到主存。UNSAFE.getObjectVolatile(...)的方式则在获取时强制当前线程从主内存中重新获取数据。所以通过unsafe的配合使用,其实也就起到了volatile的效果,保证了可见性。
关于更多unsafe类可参考:https://www.cnblogs.com/throwable/p/9139947.html
// 设置tab[i]元素=e,putOrderedObject是一个有序或者有延迟的putObjectVolatile方法,即最终会强制该操作更新到主存,但并一定立即强制,但是Java的happend-before原则会保证其一定会在其它线程get之前更新到主存。
static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i,
HashEntry<K,V> e) {
UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}
// 获取tab[i],通过Unsafe.getObjectVolatile来确保数组元素可见性。 并发情况下,如果某线程对该位置元素进行了修改或覆盖,其它线程及时可见。
static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
return (tab == null) ? null :
(HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)i << TSHIFT) + TBASE);
}
rehash扩容
//扩容,整个过程处于lock中
private void rehash(HashEntry<K,V> node) {
// 创建新数组:长度=原长度*2, 并根据加载因子计算出新数组的扩容阈值
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
// 迭代原数组元素,由于整体长度发生了变化,这里重新计算位置,由于是双倍扩容,所以会保证原处于tab[i]位置的元素,要么处于[i]处,或[i+扩容数]处。
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
// 修改table引用指向,由于table是及时可见的,所以在该赋值操作执行后,table即对其它线程可见。
// 在该操作执行之前,其它线程操作的仍是旧table
table = newTable;
}
注:整个扩容过程都处于lock中,扩容期间创建了一个新的HashEntry[],长度为原长度*2,并根据加载因子设置其新的扩容阈值。创建完毕之后,开始复制元素,由于长度发生了变化,这里需要重新定位每个元素,在这个过程中有可能破坏原有链表结构,生成新的链表结构,但由于是双倍扩容,所以元素的位置要么仍处于原有位置,要么处于原位置+新增长度的位置。如假设原长度为16,有Key的计算得到的hash=21,则扩容前该key位于21 &(16-1)= 5下标处,扩容后该key将位于 21 & (32 -1) = 21下标处(= 5 + 16)。key.hash=6的,则在扩容前后都将仍然处于6下标处。(注:扩容的是segment内部的HashEntry[],而非segments本身)
size()
当需要统计整个ConcurrentHashMap里元素的大小时,就必须统计所有Segment里元素的大小后求和。前面说过Segment中有一个全局的volatile int count变量及一个modCount变量,但在多线程并发下,并不能仅仅通过count的相加来得到总大小,原因是拿到Segent的count之后(比如这样的代码int i = s.count),如果s.count又发生了变化,就会导致统计结果不准确。所以最安全的做法,是在统计期间,将所有段都锁住,这期间的所有put、remove等操作都将处于阻塞状态,但这种方法效率并不高。
所以最终在处理size()统计时,java采用了种比较中性的方法:认为count累加过程中发生变化的几率很少但仍可能会存在,所以ConcurrentHashMap的做法是先在2次不锁的情况下进行统计,如果某次统计过程中,没有任何segment被并发修改,则返回统计得到的size。否则在第三次统计时,执行全表锁定来统计。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//这段区域的元素个数
transient volatile int count;
// 记录该段在读取期间,表被其它线程更新的次数
transient int modCount;
public int size() {
final Segment<K,V>[] segments = this.segments;
int size; // 返回的结果
boolean overflow; // true if size overflows 32 bits
long sum; // 被修改的总次数
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 尝试2次不锁定情况下进行统计,从第3次开始锁定
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; // sum+segment被修改的次数
int c = seg.count;
if (c < 0 || (size += c) < 0) // size+segment中的数量
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;
}
其它并发控制(迭代、get/containsKey/containsValue)
对于ConcurrentHashMap,迭代过程并不会抛出异常,这里不仅指单线程对Map迭代的同时执行put、remove等操作不会导致异常,还包括多线程在并发操作时也不会导致异常,比如t1在迭代,t2在put,t3在remove的情况下,对于t1来说,t2和t3的操作都是及时可见的。
从源码上来说,不管是用foreach还是Iterator,不管是keySet、entrySet等等,底层都是通过迭代器来实现的,这里以map.entrySet为例,最终迭代都会通过HashIterator来实现
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
...
final class EntryIterator extends HashIterator implements Iterator<Entry<K,V>>
{
public Map.Entry<K,V> next() {
HashEntry<K,V> e = super.nextEntry();
return new WriteThroughEntry(e.key, e.value);
}
}
abstract class HashIterator {
int nextSegmentIndex;
int nextTableIndex;
HashEntry<K,V>[] currentTable; // 迭代的当前HashEntry[]
HashEntry<K, V> nextEntry;
HashEntry<K, V> lastReturned;
// 初始化
HashIterator() {
nextSegmentIndex = segments.length - 1;
nextTableIndex = -1;
advance(); // 获取nextEntry,如果列表为空,则为null。
}
final void advance() {
for (;;) {
// 2)从链表尾部往前迭代,取该链表(即HashEntry[])第一个不为空的元素赋予nextEntry
if (nextTableIndex >= 0) {
if ((nextEntry = entryAt(currentTable,
nextTableIndex--)) != null)
break;
}
else if (nextSegmentIndex >= 0) {
// 1)迭代segments,找到第一个不为空的segment,且其HashEntry[]也不为null的segment
Segment<K,V> seg = segmentAt(segments, nextSegmentIndex--);
if (seg != null && (currentTable = seg.table) != null)
nextTableIndex = currentTable.length - 1; // 获取链表尾部索引
}
else
break;
}
}
final HashEntry<K,V> nextEntry() {
HashEntry<K,V> e = nextEntry;
if (e == null)
throw new NoSuchElementException();
lastReturned = e; // cannot assign until after null check
if ((nextEntry = e.next) == null) // 迭代到链表最后一个元素,执行一次advance()。
advance();
return e;
}
public final boolean hasNext() { return nextEntry != null; }
对上面代码作一个流程整理的话,基本如下
在执行map.entrySet()时创建一个EntrySet对象:entrySet = new EntrySet(),
再通过EntrySet的iterator()创建迭代器:it = entrySet.iterator = new EntryIterator();
EntryIterator类是HashIterator的子类,所以也会调用到HashIterator的构造器,而在这个构造器中,使用尾部向头部的方式,遍历整个segments[]数组,找到一个不为null的segment元素,且segment.hashEntry[]也不为null,且hashEntry[]含非null元素的hashEntry[],将该数组中的最后一个非null元素(从尾部向头部遍历就是第一个)赋予nextEntry,即nextEntry代表最后一个链表结构的头部。 如果集合为空或到达最后一个元素,则nextEntry最终为null,迭代器在进行hasNext()检测时将返回false,迭代操作终止。
在迭代过程中,其它线程对map的put、remove等操作都会被及时的监测到,原理在于segmentAt(Segment<K,V>[] ss, indexj),entryAt(HashEntry[] table,index)这两个方法内部使用了UNSAFE.getObjectVolatile(...)方法,该方法会强制从主存中获取最新的对象,而在上面也说过put、remove等操作底层使用了 UNSAFE.putOrderedObject(...)该操作会保证在后续获取操作之前将数据强制刷新到主存,其次hashEntry.next本身也是volatile,所以整个迭代过程中移除某个元素、或在头部插入新元素都是可见的,而且也是线程安全的。