数据结构图:
基本属性:
//用于分段
//允许的最大段数;用于绑定构造函数参数。必须是小于1小于24的幂。
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
//根据这个数来计算segment的个数,segment的个数是仅小于这个数且是2的几次方的一个数(ssize)
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//每个分段表的最小容量。一定要有2个,至少2个,以避免在懒惰的建筑后使用下一个使用。
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//用于hashEntry'
//默认的用于计算Segment数组中的每一个segment的HashEntry[]的容量,但是并不是每一个segment的HashEntry[]的容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//这个表的默认加载因子,在构造函数中没有指定的时候使用
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 用于计算Segment数组中的每一个segment的HashEntry[]的最大容量(2的30次方)
static final int MAXIMUM_CAPACITY = 1 << 30;
// segments数组,每个元素是hashEntry数组
final Segment<K,V>[] segments;
默认构造方法:
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
// 通过(1)可知ssize是segments数组的长度
int sshift = 0;
int ssize = 1;
/** 这里根据确定了ssize是大于等于concurrencyLevel与concurrencyLevel最接近的2的n次方,
sshift是1左移的次数,实际就是2的n次方的n*/
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift; // 这里用32是因为hash是32位(hash是int类型,int是32位),32-sshift相当于ssize的高位的占位数
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 计算segments数组每个元素的HashEntry数组的长度(segments数组每个元素由HashEntry数组组成)
int c = initialCapacity / ssize;
// initialCapacity无法被ssize整除,即还有剩余的HashEntry没分配到segments中去,
所以平均分配后每个segments元素里数组长度再加1
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY; // 默认segments数组每个元素的HashEntry数组的长度
// 保证segments每个元素的HashEntry数组长度是2的n次方(大于等于c最接近c的2的n次方)
while (cap < c)
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; // (1)
// segments 的第一个元素
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
put方法:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key); // hash算法计算key的hash值
int j = (hash >>> segmentShift) & segmentMask; // 计算segments数组下标
// 通过下标获取到segments对应的Segment<K,V>
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// 往s里面的HashEntry数组添加元素
return s.put(key, hash, value, false);
}
segmentShift 和 segmentMask分别是什么?假设现在采用默认构造函数 Map map = new ConcurrentHashMap();通过分析默认构造函数代码可知,segmentShift是segments数组长度(2的n次方)的高位占位数目,segmentMask = segments长度-1(值11111*); int j =(hash >>> segmentShift) & segmentMask就是hash的高位与segmentMask计算,即segments数组下标j取值为hash的高位。
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
segments的每个元素都是先初始化然后再使用。(s = (Segment<K,V>)UNSAFE.getObject(segments, (j << SSHIFT) + SBASE)) == null这个判断加上CAS保证了在高并发下只有一个线程初始化segments[j]成功, ensureSegment(j)方法会反复用到UNSAFE.getObject这个方法来确认segments[j]是否初始化了。这里需要查看Unsafe类的API,至于偏移量为啥是(j << SSHIFT) + SBASE),熟悉API后理解有也不能难,没必要深究。
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {// 再次确认segments[k]没被初始化
Segment<K,V> proto = ss[0]; // use segment 0 as prototype 使用段ss[0]作为原型
// ss[0]赋值在上面“默认构造方法”代码块倒数第二行,以ss[0]为原型是因为其他元素跟它的基本属性是一样的
// 如length和loadFactor,用这些属性来建新的segments元素
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) // 再次确认segments[k]没被初始化
== null) { // recheck
//创建Segment对象,即segments的元素
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))// 再次确认segments[k]没被初始化
== null) {
// CAS操作,这里意思是如果ss[k] == null ,就将s赋值给ss[k],如果内存中ss[k] != null,那么就继续上面的while循环,
//比如这里是从break跳出while的,说明ss[k]是由当前线程初始化的,否则说明ss[k]已被其他数组初始化了,返回已有对象即可
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
得到segments[j]后,往segments[j]添加元素:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//加锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// key在tab的位置,这里是取hash低位
int index = (tab.length - 1) & hash;
// table[index]上的第一个元素(取第一个是可能存在链表)
HashEntry<K,V> first = entryAt(tab, index);
// 遍历first
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;// 这里如果不为null,说明first是链表,继续for循环,直到e=链表最后一个元素
}
// 头为空或者first链尾
else {
// 设为表头
if (node != null)
node.setNext(first);
else // hash冲突,生成一个关联key的HashEntry
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node); // 扩容
else // 关联key的HashEntry添加到表中
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock(); // 解锁
}
return oldValue;
}
scanAndLockForPut 方法(加锁,hash冲突返回null,否则返回关联key的HashEntry):
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash); // 取表头
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
// 表头为空,直接生成新元素,后然再走while,走枷锁程序,
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
// hash冲突
else if (key.equals(e.key))
retries = 0;
// 没hash冲突
else
e = e.next;
}
// 加锁
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
// 加锁前检查表头是否改变,如果改变了就重新走一遍scanAndLockForPut方法
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
扩容(扩容后元素新位置等于原位置或原位置+原数组长度):
// node扩容后添加到新的数组中
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1; // 扩容为2倍
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity]; // 创建新数组
int sizeMask = newCapacity - 1; // 新的掩码111*,假设新数组长度是2的n次方,新的掩码占位数目就是是n,
for (int i = 0; i < oldCapacity ; i++) { // 遍历老数组,将原数组位置 i 处的链表拆分到新数组位置 i 和 i+oldCap 两个位置
HashEntry<K,V> e = oldTable[i];// e 是链表的第一个元素
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; // e 是链表表头
int lastIdx = idx; // idx 是当前链表的头结点 e 的新位置
/**下面这个 for 循环会找到一个 lastRun 节点,这个节点之后的
所有元素是将要放到一起的 */
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
/** 计算新数组链表的位置,hash & sizeMask要么位置不变,要么就是原
位置+原数组长度*/
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
// 将 lastRun 及其之后的所有节点组成的这个链表放到 lastIdx 这个位置
newTable[lastIdx] = lastRun;
// Clone remaining nodes
/**下面的操作是处理 lastRun 之前的节点(lastRun在原链表中是尾部连续的一块),这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中,由k决定*/
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);
}
}
}
}
// 将新来的 node 放到新数组中刚刚的 两个链表之一的头部(头插法),
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
实现过程:先对数组的长度增加一倍,然后遍历原来的旧的table数组,把每一个数组元素也就是Node链表迁移到新的数组里面,最后迁移完毕之后,把新数组的引用直接替换旧的。此外这里这有一个小的细节优化,在迁移链表时用了两个for循环,第一个for的目的是为了,判断是否有迁移位置一样的元素并且位置还是相邻,根据HashMap的设计策略,首先table的大小必须是2的n次方,我们知道扩容后的每个链表的元素的位置,要么不变,要么是原table索引位置+原table的容量大小。由于采用头插法,元素位置倒置。
此处参考:https://www.cnblogs.com/lfs2640666960/p/9621461.html