1、HashMap
1.1、为什么HashMap非线程安全的
(1)竞态条件
- 当多个线程同时对 HashMap 进行写操作(如插入、删除、修改),由于没有同步控制,可能会导致数据不一致的情况。
- 例如,两个线程同时向同一个空的 HashMap 插入不同的键值对,由于没有互斥操作,它们可能会同时触发扩容操作,导致其中一个线程的插入操作被覆盖或丢失。
(2)死循环(JDK1.8 之前)
- 在多线程环境下,如果一个线程正在进行 HashMap 的结构调整(如扩容),而另一个线程正在进行读取或修改操作,可能会导致读取或修改操作陷入死循环。
- 这是因为在结构调整期间,HashMap 的链表结构可能会发生变化,而另一个线程可能无法正确地遍历或定位到元素。
死循环示例
- 前置知识
- 死循环执行步骤
(3)JDK1.8 开始便没有了死循环问题
- JDK8 将 HashMap 的结构作了下修改,将原来的链表部分改为数据少时仍然链表,当超过一定数量后变换为红黑树。
- 红黑树:一种自平衡的二叉搜索树(Binary Search Tree),它在每个节点上增加了一个额外的属性来维持平衡。红黑树的平衡性质使得它在插入、删除和查找等操作的时间复杂度上具有较好的保证,为O(log n)。
通过上面的分析,不难发现循环的产生是因为新链表的顺序跟旧的链表是完全相反的,所以只要保证建新链时还是按照原来的顺序的话就不会产生循环。
- JDK8是用 head 和 tail 来保证链表的顺序和之前一样,这样就不会产生循环引用。
1.2、保证HashMap线程安全的方案
- 【方案1】使用Hashtable线程安全类
- 【方案2】使用Collections.synchronizedMap方法,对方法进行加同步锁
- 【方案3】使用并发包中的ConcurrentHashMap类
2、HashTable
2.1、HashTable性能问题
- Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!
- 相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!
2.2、Collections.synchronizedMap性能问题
- Collections.synchronizedMap 里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!
- 使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用!
3、ConcurrentHashMap
CHM是弱一致性的
- 添加元素后,不一定马上读到
- 清空之后,可能仍然会有元素
- 遍历之前的变化,可以读到
- 遍历之后的变化,无法读到
- 遍历时元素发生变化,不抛异常
3.1、JDK1.5、JDK1.6分段锁
- JDK1.5对key进行hash,然后用hash值的高位查找segment[],低位则查找table[]
- JDK1.6的优化是得到的hash值高位和低位在segment[]和table[]分布更均匀
- 分16段,所以在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作
3.2、JDK1.7分段锁懒加载
- JDK1.5、JDK1.6是直接初始化16个segment
- JDK1.7则是用到哪个segment再初始化哪个,没用到的不初始化
- JDK1.7的segment使用volatile修饰
3.3、JDK1.8摒弃分段锁
- volatile直接修饰table
- 加锁也直接加到table上
为什么HashMap会产生死循环
多线程下的HashMap死循环问题详解
彻头彻尾理解 ConcurrentHashMa
一文彻底弄懂ConcurrentHashMap
ConcurrentHashMap