本文只是学习笔记,如有错误,欢迎指出
ConcurrentHashMap包含若干个Segment组成的数组,每个Segment包含一个table数组,而table是若干HashEntry链表组成的数组
HashEntry类,并没有像HashMap那样实现Map.Entry类因为value成员变量是volatile的,不是final,
对于非同步读取,通过数据竞争进行读取时得到null而不是初始值是有可能的
其他成员变量都是final修饰
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}
Segment类继承ReentrantLock,充当锁的角色,来守护对table数组的修改,所以修改ConcurrentHashMap,
只需要锁住对应的Segment就行,不会影响其他Seqment的修改
static final class Segment<K,V> extends ReentrantLock {
...
//注意这个count是volatile的,在put等修改操作完成后会去设置count值,
//而get时会去读取这个值
//根据volatile 变量法则:对volatile域的写入操作happens-before于每个后续对同一volatile 的读操作
//所以读取的时候都是被修改过的
transient volatile int count;
transient volatile HashEntry<K,V>[] table;
...
}
构造器
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;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
//concurrencyLevel是并发级别,默认是16
//在理想状态下,ConcurrentHashMap可以支持16个线程执行并发写操作,如果concurrencyLevel为16
//因为通过计算segments的数组大小为16,如果每个线程修改的是不同segments,则不会被阻塞
//计算segments数组的大小,至少要大于concurrencyLevel并为2的幂次方
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
//默认参数下,上面的计算
//sshift = 4
//ssize = 16
//segmentShift = 28
//segmentMask = 0x1111
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//计算每个segments里的table数组的大小
//c = 16/16 = 1
//cap不小于1,所以大小为1
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = 1;
while (cap < c)
cap <<= 1;
for (int i = 0; i < this.segments.length; ++i)
this.segments[i] = new Segment<K,V>(cap, loadFactor);
}
put操作,通过hash将key的hashcode再计算一次
public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());
//通过hash找到key所在的seqment,再通过hash找到table数组的位置
return segmentFor(hash).put(key, hash, value, false);
}
//默认参数情况下是hash>>>28 & 0x1111
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
Segment的put方法
V put(K key, int hash, V value, boolean onlyIfAbsent) {
//put操作时先锁住
lock();
try {
//读取count值
int c = count;
//当小于threshold时,扩充数组
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
//计算位于table数组的下标
int index = hash & (tab.length - 1);
//获取位于index的HashEntry
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
//查找是否存在相同的key
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {//如果e不为null,代表存在相同的key值,根据参数决定是否替换掉旧值
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {//不存在相同key值则创建一个HashEntry,旧的链表指向这个HashEntry,并放在table数组的index位置上
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
//写入count,这样get等读取操作总是能读取到count修改后的值
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
扩充数组
参考https://blog.csdn.net/wei83523408/article/details/52717942
void rehash() {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity >= MAXIMUM_CAPACITY)
return;
//扩容为原来的2倍
HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1);
threshold = (int)(newTable.length * loadFactor);
//mask相当于旧的mask左移1位
int sizeMask = newTable.length - 1;
//将旧的数组元素放置到新的数组里
for (int i = 0; i < oldCapacity ; i++) {
// We need to guarantee that any existing reads of old Map can
// proceed. So we cannot yet null out each bin.
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
//如果next为空,说明该链表只有一个元素,直接放置到新的位置上
if (next == null)
newTable[idx] = e;
else {
//如果链表超过一个元素,则查找链表的元素是否可以同时移动,且在链表的前后顺序不会改变
//假如原来有大小为2^k,扩容后大小为2^(k+1)
//扩容后idx只有两种可能,要么idx仍然为旧的idx,要么为idx+2^k
//举个例子,假如k=2,原来有大小为4,扩容后大小为8
//原来的sizeMask为0x11,扩容后的为0x111
//如果原来的hash为0x101,扩容前的idx为0x1,扩容后为0x101=0x1 + 2^2
//若果原来的hash为0x1,则扩容前后idx不变
//这里会在链表里找到可以移动到同一位置的子链表
//接上个例子,如果这个链表里的元素hash分别为0x101(A),0x001(B),0x101(C),0x101(D)
//则通过这个循环,lastIdx=0x101,lastRun=C
//因为D的idx和C的一样,所以他们在新的数组里的位置也是一样的
//因此可以直接把C放到新数组idx位置上
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;
}
}
//将找到的链表放到新的位置上,因为这个位置要么就是正在处理的当前位置,
//要么就是当前位置加上原来大小的新位置(idx+2^k),所以不需要处理这个位置上本来的元素
newTable[lastIdx] = lastRun;
//处理上面没被移动过的元素,且放到链表头部
// Clone all remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
int k = p.hash & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(p.key, p.hash,
n, p.value);
}
}
}
}
table = newTable;
}
读操作
V get(Object key, int hash) {
//对count变量进行读取,保证读取到count被修改后的值,这里并没有锁住Segment
if (count != 0) { // read-volatile
//在table数组中获取hash对应的HashEntry
HashEntry<K,V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
//因为读到的是null值,而ConcurrentHashMap是不允许key或者value为null的
//value变量是volatile的,说明这里发生了数据竞争,需要加锁在读一次
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
删除操作
V remove(Object key, int hash, Object value) {
lock(); // 加锁
try{
int c = count - 1;
HashEntry<K,V>[] tab = table;
// 根据hash找到 table 的下标值
int index = hash & (tab.length - 1);
// 找到hash对应的那个HashEntry
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while(e != null&& (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue = null;
if(e != null) {
V v = e.value;
if(value == null|| value.equals(v)) { // 找到要删除的节点
oldValue = v;
++modCount;
// 所有处于待删除节点之后的节点原样保留在链表中
// 所有处于待删除节点之前的节点被克隆到新链表中,且在链表的顺序被反转了
HashEntry<K,V> newFirst = e.next;// 待删节点的后继结点
for(HashEntry<K,V> p = first; p != e; p = p.next)
newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value);
// 链接到新的头结点
// 新的头结点是原链表中,删除节点之前的那个节点
tab[index] = newFirst;
count = c; //写 count 变量
}
}
return oldValue;
} finally{
unlock(); // 解锁
}
}
处理删除节点前的节点时,并没有直接像HahsMap那样通过修改节点的next来修改连接,这里next变量是final的,也修改不了,而是通过new HashEntry来创建新的节点
所以在执行remove操作时,原始链表并没有被修改,也就是说,读线程不会受同时执行remove操作的并发写线程的干扰
ConcurrentHashMap的高并发性主要来自于三个方面:
1 用分离锁实现多个线程间的更深层次的共享访问。(通过分Segment,减少锁竞争)
2 用HashEntery对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。(HashEntery的成员变量除了value都是final的)
3 通过对同一个volatile变量的写/读访问,协调不同线程间读/写操作的内存可见性。(put/get等操作,对Segment.count成员变量进行读写)