HashMap 基本实现(JDK 8 之前)

HashMap 通常会用一个指针数组(假设为 table[])来做分散所有的 key,当一个 key 被加入时,会通过 Hash 算法通过 key 算出这个数组的下标 i,然后就把这个 <key, value> 插到 table[i] 中,如果有两个不同的 key 被算在了同一个 i,那么就叫冲突,又叫碰撞,这样会在 table[i] 上形成一个链表

 
 

如果 table[] 的尺寸很小,比如只有 2 个,如果要放进 10 个 keys 的话,那么碰撞非常频繁,于是一个 O(1) 的查找算法,就变成了链表遍历,性能变成了 O(n),这是 Hash 表的缺陷

所以,Hash 表的尺寸和容量非常的重要。一般来说,Hash 表这个容器当有数据要插入时,都会检查容量有没有超过设定的 threshold,如果超过,需要增大 Hash 表的尺寸,但是这样一来,整个 Hash 表里的元素都需要被重算一遍。这叫 rehash,这个成本相当的大

HashMap 的 rehash 源代码

Put 一个 Key, Value 对到 Hash 表中:

public V put(K key, V value) {
......
// 计算 Hash 值
int hash = hash(key.hashCode());
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++;
// 该 key 不存在,需要增加一个结点
addEntry(hash, key, value, i);
return null;
}

检查容量是否超标

void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//查看当前的 size 是否超过了设定的阈值 threshold,如果超过,需要 resize
if (size++ >= threshold)
resize(2 * table.length);
}

新建一个更大尺寸的 hash 表,然后把数据从老的 Hash 表中迁移到新的 Hash 表中

void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
// 创建一个新的 Hash Table
Entry[] newTable = new Entry[newCapacity];
// 将 Old Hash Table 上的数据迁移到 New Hash Table 上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}

迁移的源代码

void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}

该方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生 get() 死循环。所以只要保证建新链时还是按照原来的顺序的话就不会产生循环(JDK 8 的改进)

正常的 ReHash 的过程

  • 假设我们的 hash 算法就是简单的用 key mod 一下表的大小(也就是数组的长度)
  • 最上面的是 old hash 表,其中的 Hash 表的 size = 2,所以 key = 3, 7, 5,在 mod 2 以后都冲突在 table[1] 这里了
  • 接下来的三个步骤是 Hash 表 resize 成 4,然后所有的 <key, value> 重新 rehash 的过程
 
 

并发下的 Rehash

1)假设有两个线程

do {
Entry<K,V> next = e.next; // 假设线程一执行到这里就被调度挂起了
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),其在线程二 rehash 后,指向了线程二重组后的链表。可以看到链表的顺序被反转

2)线程一被调度回来执行

  • 先是执行 newTalbe[i] = e;
  • 然后是 e = next,导致了 e 指向了 key(7)
  • 而下一次循环的 next = e.next 导致了 next 指向了 key(3)
 
 

3)线程一接着工作。把 key(7) 摘下来,放到 newTable[i] 的第一个,然后把 e 和 next 往下移

 
 

4)环形链接出现
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
此时的 key(7).next 已经指向了 key(3), 环形链表就这样出现了

 
 

JDK 8 的改进

JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树

HashMap 不会因为多线程 put 导致死循环(JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)。因此多线程情况下还是建议使用 ConcurrentHashMap

为什么线程不安全

HashMap 在并发时可能出现的问题主要是两方面:

  • 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖

  • 如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失

作者:杰哥长得帅
链接:https://www.jianshu.com/p/619a8efcf589
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

HashMap 在高并发下引起的死循环的更多相关文章

  1. HashMap在高并发下引起的死循环

    HashMap事实上并非线程安全的,在高并发的情况下,是非常可能发生死循环的,由此造成CPU 100%,这是非常可怕的.所以在多线程的情况下,用HashMap是非常不妥当的行为,应採用线程安全类Con ...

  2. HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么

    Hashmap在并发环境下,可能出现的问题: 1.多线程put时可能会导致get无限循环,具体表现为CPU使用率100%: 原因:在向HashMap put元素时,会检查HashMap的容量是否足够, ...

  3. HashMap高并发下存在的问题

    原文链接:https://blog.csdn.net/bjwfm2011/article/details/81076736 1.什么是HashMap? HashMap底层原理 HashMap是存储键值 ...

  4. 对HashMap的理解(二):高并发下的HashMap

    在分析hashmap高并发场景之前,我们要先搞清楚ReHash这个概念.ReHash是HashMap在扩容时的一个步骤.HashMap的容量是有限的.当经过多次元素插入,使得HashMap达到一定饱和 ...

  5. JDK1.7 ConcurrentHashMap--解决高并发下的HashMap使用问题

    高并发下也可以使用HashTable .Collections.synchronizedMap因为他们是线程安全的,但是却牺牲了性能,无论是读操作.写操作都是给整个集合加锁,导致同一时间内其他操作均为 ...

  6. JDK1.7 高并发下的HashMap

    HashMap的容量是有限的.当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高. 这时候,HashMap需要扩展它的长度,也就是进行Resize. 影响发 ...

  7. 高并发下,HashMap会产生哪些问题?

    HashMap在高并发环境下会产生的问题 HashMap其实并不是线程安全的,在高并发的情况下,会产生并发引起的问题: 比如: HashMap死循环,造成CPU100%负载 触发fail-fast 下 ...

  8. 高并发下的Java数据结构(List、Set、Map、Queue)

    由于并行程序与串行程序的不同特点,适用于串行程序的一些数据结构可能无法直接在并发环境下正常工作,这是因为这些数据结构不是线程安全的.本节将着重介绍一些可以用于多线程环境的数据结构,如并发List.并发 ...

  9. Java高并发下多线程编程

    1.创建线程 Java中创建线程主要有三种方式: 继承Thread类创建线程类: 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务.因此也把run方法称为 ...

随机推荐

  1. ol li 兼容

    ol 标签在 chrome 60 和 safari12 缩进不一样. 因为序号距离copy距离不一样,导致显示不一样.解决办法. list-style-position: inside;text-in ...

  2. element-ui 日期选择器范围时间限制

    来自 https://www.cnblogs.com/xjcjcsy/p/7977966.html 侵删 ElementUI是饿了么推出的一套基于vue2.x的一个ui框架.官方文档也很详细,这里做一 ...

  3. An incompatible version [1.2.10] of the APR based Apache Tomcat Native library is installed, while Tomcat requires version [1.2.14]

    这个链接的博主写的很详细,直接推荐:https://blog.csdn.net/zhoukikoo/article/details/80532483

  4. Nginx作为缓存服务

    缓存类型 (1) 服务器缓存 服务端缓存一般使用Memcache.Redis (2)代理缓存 (3)客户端缓存 代理缓存流程图 第一步:客户端第一次向Nginx请求数据a: 第二步:当Nginx发现缓 ...

  5. springboot 打包发布(war包)

    版本关系: 软件名称 版本号 软件名称 版本号 spring boot 2.x jdk 1.8 tomcat 9.x springboot中的pom.xml文件 打包:右键点击项目,选择如下图: 填写 ...

  6. CIP 协议安全扫盲

  7. Linux命令——ldd和ldconfig

    转自:Linux系统中“动态库”和“静态库”那点事儿 前言 在调试lua脚本的时候,报错. 我已经再lua脚本中更改了cpath package.cpath = package.cpath .. &q ...

  8. C#预处理器指令——学习

    若要详细了解如何使用 C# 预处理器指令选择性地编译代码段,请参阅 #define(C# 参考)和 #if(C# 参考). #define(C# 参考) 地址:https://docs.microso ...

  9. 小程序数据绑定和setData

    我们wxml没有直接调用数据的能力,我们的逻辑是通过js调用数据,再由js传递给wxml才能够显示出来.那么怎么由js传递给wxml?   首先我的js里面有这样一段代码 process: funct ...

  10. IDEA实用教程(三)

    4. JDK环境的配置 1) 进入JDK配置界面 2) 创建JDK环境 3) 选择本地JDK的安装位置 4) 保存配置 点击右下角的Apply后,再点击OK保存配置