问题描述
问题 : 服务器cpu被100%的线上故障,是因为java语言在并发情况下使用了HashMap造成Receive Condition (竞态条件 : 并发访问出现冲突,结果不正确)
首先HashMap是非线程安全
的数据结构,在单线程的程序中使用,是没有问题的.但是在多线程程序中,就可能出现问题.
HashMap回顾
- HashMap 通常会使用一个数组(table[] 来分散所有的key),当一个key被加入时 ,会通过hash算法会通过key计算出key要放入数组位置的下标(i),然后将<key,value> 插到tablep[i] 中 , 如果两个key算出来同一个i,那么这就叫冲突,在HashMap中采用链地址法解决冲突,因而在数组的每个节点的下面会形成一个链表.
- table[]的初始大小很小,只有
16
,但是需要添加大量数据时,碰撞就会变得频繁.所以就规定了一个阈值(thredhold
),如果超过就需要扩大table[]的大小,但是, 整个hash表里面的元素位置都要重新计算一遍(rehash
),这个花费的成本相当大.
rehash源码
添加一个<key,value>到Hash表中
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 计算i
int i = indexFor(hash, table.length);
//判断添加的key是否在链表中,如果已经存在,替换掉原来的value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 没有找到添加新的节点
addEntry(hash, key, value, i);
return null;
}
检查容量是否超标
void addEntry(int hash, K key, V value, int bucketIndex) {
// 判断size是否大于阈值和计算出的i位置是否为空,满足则需要resize
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
新建hash表,将旧表中的数据转移至新表中
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建新的table
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
// 将旧table的数据转移到新的table中
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
迁移源码
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历oldtable中的每一个元素,将其放入newtable中
for (Entry<K,V> e : table) {
while(null != e) { //当链表中的节点不为空时
Entry<K,V> next = e.next; //遍历当前链表的下一个节点
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//此处每次都将新节点写入到 newTable[i] 中,用e.next将先前写入的节点保存起来
e.next = newTable[i]; // 保存先前写入的节点
newTable[i] = e; // 将节点写入 newTable[i] = e
e = next;
}
}
}
正常情况的rehash
- 首先最开始的size 大小为2 ,简单的用 key mod size 算出其 i 值
- 接下来将hash表resize为 4 ,将所有的<key,value >重新rehash
并发下的rehash
- 我们有两个进程,进程1被阻塞到如下位置
do {
Entry<K,V> next = e.next; //当线程1执行到这里被调度挂起
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
进程二执行完了
因为Thread1 的e 指向了 key :3 ,而next 指向了key :7 .而在线程二rehash1 ,Thread1的e指向了key:7,next指向了key: 3
2. 线程1被调度执行
- 首先执行的是 newTable[i] = e ; (null)
- 然后是e = next ; (导致e指向 key : 7)
- 而下一次的循环中 next指向了key : 3
- 将key :7 ,放到newTable[i]的第一个位置,然后将e 和next往下移
- 环形链接出现
- 此时的 e.next = newTable[i];保存的是 (key : 7) ;
- 在此之后会一直循环下去
参考
https://coolshell.cn/articles/9606.html