ConcurrentHashMap弱一致的迭代器的原理是:ConcurrentHashMap 的弱一致性迭代器是基于快照迭代器实现的。在 ConcurrentHashMap 中,每个 Segment 维护了一个元素计数器 modCount 和一个修改次数计数器 count。
一、ConcurrentHashMap弱一致的迭代器是什么原理
ConcurrentHashMap 的弱一致性迭代器是基于快照迭代器实现的。在 ConcurrentHashMap 中,每个 Segment(即 ConcurrentHashMap 中每个 key-value 对所在的 segment)维护了一个元素计数器 modCount 和一个修改次数计数器 count。当 segment 发生修改操作时,count 计数器会自增,同时 modCount 计数器也会自增。当使用迭代器对 ConcurrentHashMap 进行遍历时,迭代器会在每次访问之前,记录当前 modCount 值,若在迭代过程中 ConcurrentHashMap 发生了修改操作,modCount 值会发生变化,迭代器会发现这个变化,会抛出 ConcurrentModificationException 异常。
因此,ConcurrentHashMap 迭代器的弱一致性体现在它不保证遍历结果即使在迭代过程中 ConcurrentHashMap 发生了修改,但它保证迭代器不会因为在遍历时 ConcurrentHashMap 包含了新元素而抛出异常。同时这种设计也能保证在大多数情况下,迭代器访问的元素都不会太过陈旧,可以满足迭代器的绝大部分使用场景。
二、ConcurrentHashMap简介
java.util.concurrent.ConcurrentHashMap 属于 JUC 包下的一个集合类,可以实现线程安全。它由多个 Segment 组合而成。Segment 本身就相当于一个 HashMap 对象。同 HashMap 一样,Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。
ConcurrentHashMap的put方法流程(JDK1.7):
public V put(K key, V value) {
//key,value不能为null
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
//通过key进行哈希到对应segment位置
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
//通过位置j获取当前的对应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);
}
#内部类Segment下的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//尝试性加锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//当前segment下的table
HashEntry<K,V>[] tab = table;
//通过key的哈希值进行哈希找到对应table位置
int index = (tab.length - 1) & hash;
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) {
//put方法处理:将新value替换oldvalue
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;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//超过扩容阈值
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//释放锁
unlock();
}
return oldValue;
}
//扩容仅针对某个segment进行扩容,而不是对整个ConcurrentHashMap进行扩容
private void rehash(HashEntry<K,V> node) {
//在segment下的table
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
//按照原大小2倍关系进行扩容
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
//将原有table上的所有hashentry节点进行重新哈希到新table上
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 = newTable;
}
三、ConcurrentHashMap和HashMap的区别
ConcurrentHashMap和HashMap的区别如下:
- HashMap是非线程安全的;而ConcurrentHashMap是线程安全的。
- HashMap的key和value均可以为null;而ConcurrentHashMap的key和value均不可以为null。
- HashMap是通过给整张散列表加锁的方式来保证线程安全,这种方式保证了线程安全,但是并发执行效率低下;ConcurrentHashMap在JDK1.8之前,采用分段锁机制来保证线程安全的,这种方式可以在保证线程安全的同时,一定程度上提高并发执行效率(当多线程并发访问不同的segment时,多线程就是完全并发的,并发执行效率会提高)。但从JDK1.8开始,ConcurrentHashMap数据结构与1.8中的HashMap保持一致,均为数组+链表+红黑树,是通过乐观锁+Synchronized来保证线程安全的。当多线程并发向同一个散列桶添加元素时。若散列桶为空,此时触发乐观锁机制,线程会获取到桶中的版本号,在添加节点之前,判断线程中获取的版本号与桶中实际存在的版本号是否一致,若一致,则添加成功,若不一致,则让线程自旋。
延伸阅读1:JDK1.7 与 JDK1.8 中 ConcurrentHashMap 的区别
- 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
- 保证线程安全机制:JDK1.7 采用Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8采用CAS+synchronized保证线程安全。
- 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8调整为对每个数组元素加锁(Node)。
- 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash冲突加剧,因此在链表节点数量大于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
- 查询时间复杂度:从JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。