概述
ConcurrentHashmap是一个支持并发检索和并发更新的线程安全的HashMap,它是不支持空key和value的。ConcurrentHashMap在JDK1.7之前使用的Lock和Segment(分段锁)来实现并发安全的,JDK1.8改用CAS和synchronized来实现的。我们这次主要分析JDK1.8中的实现方式。
简单使用
ConcurrentHashMap在使用上和我们平时常用的HashMap差异不是很大。只不过一个支持并发操作,一个不支持并发操作而已。
下面我们就写一个非常非常简单的例子:
1 | public static void main(String[] args) { |
下面我们就针对这个例子,分析一下其内部实现。
源码分析
数据结构

从图中我们可以看出有许多种的节点类。那么我们下面就分别解释一波这些节点类。
Node<K,V>这个节点类是最基础的,它存储了键值对(值使用了volatile关键字确保可见性),hash值,下一个节点的引用。
TreeNode<K,V>,这个节点类表示红黑树节点,当链表的长度大于等于8,且数组的长度大于64的时候,就会将链表节点转换为红黑树节点,然后将这些红黑树节点放到TreeBin对象中,由TreeBin对象来完成对红黑树的封装。
TreeBin<K,V>,封装了红黑树根节点。
ForwardingNode<K, V>,在节点转移的时候,用于连接两个table的节点类。它的内部包含一个nextTable指针,指向下一个table。这个节点仅仅作为占位节点表示当前节点已经被移动。
ReservationNode<K,V>这个也是一个占位节点,表示当前节点已经被占用。
重要属性
1 | /*最大容量*/ |
重要方法源码分析
put方法
put方法,实际上是调用了V putVal(K key, V value, boolean onlyIfAbsent)
1 | public V put(K key, V value) { |
V putVal(K key, V value, boolean onlyIfAbsent)的具体实现如下:
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
下面我们就梳理一下,整个put操作的整个流程:
- 计算key的hash值,并根据hash值计算索引i
- 如果但其哈希表还未初始化就调用
initTable()进行初始化 - 如果索引i的位置为空,那么就直接CAS将当前节点放入该位置即可。
- 如果当前节点的hash值为-1,即当前节点处于移动状态,那么就调用
helpTransfer(tab, f)帮助扩容 - 如果不满足上述两种情况,那么使用synchronized进行加锁后,进行hash冲突处理。
- 如果位置i是个链表,那么就遍历整个链表,如果在遍历的过程中,发现了某个节点的hash值与当前key的哈希值相同,那么就覆盖该节点,并停止遍历。如果到了链表尾部了,那么就创建一个新的节点加入链表尾部。
- 如果位置i上为
TreeBin。说明这个位置是红黑树,那么就调用putTreeVal方法,要么覆盖某个节点,要么创建一个新节点加入。
- 插入完毕之后,如果是链表的话会检查链表的长度,如果达到链表转红黑树的阈值的话,会进行链表转红黑树的操作
- 最后调用
addCount方法,更新元素的数量
把整个流程看下来,我们发现在整个流程中,还有几个方法起到了非常重要的作用。
它们分别是initTable(),helpTransfer(tab, f), treeifyBin(tab, i);,addCount(1L, binCount);下面我们就仔细分析一波这些方法。
initTable方法
1 | private final Node<K,V>[] initTable() { |
在初始化过程中,首先会判断是否有线程正在初始化,如果正在初始化,那么就让出CPU时间片,自旋等待创建成功。如果没有线程正在初始化,那么该线程就会开始初始化。
那么是如和判断是否有其它线程正在初始化,和保障自己在创建过程中其它线程不会创建呢?
这个时候sizeCtl起到了非常重要的作用,一个线程开始初始化就会将sizeCtl设置为-1,这样其它线程就可以以此判断已经有线程在初始化了。初始化完成之后,会将sizeCtl设置为0.75*n。
helpTransfer方法
这个方法的作用是帮助其它线程进行转移操作
1 | final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { |
transfer方法
transfer方法的作用,主要是转移或复制节点到新的table
1 | private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { |
因为ConcurrentHashMap的扩容,实际上是新建了一个table,因此扩容最主要的任务就是将旧table中的节点转移到新的table中。
在以下三种情况下,是需要进行转移的:
- 对table进行扩容的时候
- 在调用
addCount方法更新元素的数量的时候,发现元素的数量已经达到扩容的阈值的时候。 - 在进行put操作的时候,发现需要加入的位置的节点正在进行转移的时候,那么当前线程会帮助扩容。
在整个转移的过程中,有两个比较重要的地方,其中一个是transferIndex,它的初始值是最后一个节点,它的含义是:从transferIndex到最后一个节点的转移任务已经被领取。
还有一个是forwardNode节点,它用于标记已经处理过的位置。
addCount方法
1 | private final void addCount(long x, int check) { |
treeifyBin
1 | private final void treeifyBin(Node<K,V>[] tab, int index) { |
get方法
1 | public V get(Object key) { |
get方法的实现与hashmap的实现差不多,就不赘述了。