一、摘要
在之前的集合文章中,我们了解到 HashMap 在多线程环境下操作可能会导致程序死循环的线上故障!
既然在多线程环境下不能使用 HashMap,那如果我们想在多线程环境下操作 map,该怎么操作呢?
想必阅读过小编之前写的《HashMap 在多线程环境下操作可能会导致程序死循环》一文的朋友们一定知道,其中有一个解决办法就是使用 java 并发包下的 ConcurrentHashMap 类!
今天呢,我们就一起来聊聊 ConcurrentHashMap 这个类!
二、简介
众所周知,在 Java 中,HashMap 是非线程安全的,如果想在多线程下安全的操作 map,主要有以下解决方法:
- 第一种方法,使用
Hashtable
线程安全类; - 第二种方法,使用
Collections.synchronizedMap
方法,对方法进行加同步锁; - 第三种方法,使用并发包中的
ConcurrentHashMap
类;
在之前的文章中,关于 Hashtable 类,我们也有所介绍,Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized
同步锁!
相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!
再来看看第二种方法,使用Collections.synchronizedMap
方法,我们打开 JDK 源码,部分内容如下:
可以很清晰的看到,如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!
使用Collections.synchronizedMap
方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用!
上面2种方法,由于都是对方法进行全表锁,所以在多线程环境下容易造成性能差的问题,因为hashMap 是数组 + 链表的数据结构,如果我们把数组进行分割多段,对每一段分别设计一把同步锁,这样在多线程访问不同段的数据时,就不会存在锁竞争了,这样是不是可以有效的提高性能?
再来看看第三种方法,使用并发包中的ConcurrentHashMap
类!
ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁)
,这个小数组名叫Segment
, 如下图:
当然,JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!
JDK1.8 对 HashMap 做了改造,当冲突链表长度大于8时,会将链表转变成红黑树结构,上图是 ConcurrentHashMap 的整体结构,参考 JDK1.7!
我们再来看看 JDK1.8 中 ConcurrentHashMap 的整体结构,内容如下:
JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS
+ synchronized
来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于8时,链表结构转为红黑二叉树)结构。
ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!
说了这么多,我们再一起来看看 ConcurrentHashMap 的源码实现。
三、JDK1.7 中的 ConcurrentHashMap
JDK 1.7 的 Concur