前言
HashMap是我们开发中使用最频繁的集合,但是它是非线程安全的,在涉及到多线程并发的情况时,put操作有可能会引起死循环。
解决方案有HashTable
和Collections.synchronizedMap(hashMap)
,它们是线程安全的,它们的实现原理是在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。相对于粗暴的锁住整个table的做法,ConcurrentHashMap则使用了一种 锁分段技术
,实现并发的更新操作。底层采用 Node数组+链表+红黑树
的存储结构。
源码解析
内部数据结构
1 | public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> |
ConcurrentHashMap
的底层数据结构为:Node数组 + 链表 + 红黑树
,因此ConcurrentHashMap
的内部数据主要有:
- 内部类Node:Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据。
- 内部类TreeNode:TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构。
- 内部类TreeBin:封装TreeNode的容器。在树底层结构存放的是TreeBin对象,而不是TreeNode对象。
- 内部类ForwardingNode:一个特殊的节点,hash值为-1,其中存储nextTable的引用。只有table发生扩容时,ForwardingNode才会发挥作用。
内部类源码分析
Node内部类
1 | /** |
Node数据结构很简单,从上可知,就是一个链表,但是只允许对数据进行查找,不允许进行修改
。
TreeNode内部类
1 | /** |
TreeBin内部类
1 | static final class TreeBin<K,V> extends Node<K,V> { |
TreeBin是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制。在链表需要转为树结构时,会使用到此类。
ForwardingNode内部类
1 | //一个特殊的节点,hash值为-1,其中存储nextTable的引用。只有table发生扩容时,ForwardingNode才会发挥作用。 |
只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。
ConcurrentHashMap源码解析
构造函数
1 | //无参构造函数 |
在有参的ConcurrentHashMap
构造函数中,主要的功能是初始化 sizeCtl
的值。
put 源码
1 | public V put(K key, V value) { |
由源码可知,其内部调用了putVal()方法
putVal源码
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
putVal()方法的整理流程为:
- key,value是否为空,为空,抛异常,结束。非空,执行2
- 将key二次hash,算出hash值
遍历hash表。
1)判断hash表是否为初始化,如果为初始化,则进行初始化。
2)算出待插入元素在hash表中的索引
index=(length-1) & hash
,若索引index处元素 (f = tab[index]) == null,则直接CAS进行插入操作,成功,跳出循环。3)如果
f.hash == -1
,说明有其他线程正在扩容,则一起进行扩容操作。4)否则,将元素插入数组中。在插入数据时,首先将f节点加锁(synchronized),如果
f.hash > 0
,则说明f为链表结构,则进行链表插入操作;如果f instanceof TreeBin
为树结构,则进行数的相应插入操作。插入完成后,判断是否需要转为树结构。最后跳出循环。元素插入成功后,检查是否需要进行扩容。
- 方法结束。
spread源码
1 | static final int spread(int h) { |
该方法的主要功能是给定的值进行两次hash,减少hash冲突,可以均匀分布。
initTable源码
1 | private final Node<K,V>[] initTable() { |
sizeCtl默认为0,如果ConcurrentHashMap实例化时有传参数,sizeCtl会是一个2的幂次方的值。所以执行第一次put操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl为-1,有且只有一个线程能够修改成功,其它线程通过Thread.yield()让出CPU时间片等待table初始化完成。
helpTransfer源码
1 | //在多线程情况下,如果发现其它线程正在扩容,则帮助转移元素。 |
其实helpTransfer()方法的目的就是调用多个工作线程一起帮助进行扩容,这样的效率就会更高。
resizeStamp源码
1 | static final int resizeStamp(int n) { |
transfer源码
1 | private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { |
哦,my god,太复杂,看不懂。🤦
treeifyBin源码
1 | //将数组下标为index上的元素,在必要的情况下转换为树结构。 |
在成功插入元素后,如果插入位置对应的链表中的节点数大于等于8,则将链表转为红黑树结构。在转红黑树结构时,若hash表当前容量小于64,则不转数,只进行两倍扩容即可。在当前容量大于64时,通过加锁,将链表转为树结构。
addCount源码
1 | private final void addCount(long x, int check) { |
数据加入成功后,现在调用addCount()方法计算ConcurrentHashMap的size,在原来的基础上加一
get源码
1 | public V get(Object key) { |
ConcurrentHashMap的get操作的流程很简单,也很清晰,可以分为三个步骤来描述
- 计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
size源码
1 | public int size() { |
sumCount源码
1 | final long sumCount() { |
在JDK1.8版本中,对于size的计算,在扩容和addCount()方法就已经有处理了。而JDK1.8版本中,ConcurrentHashMap还提供了另外一种方法可以获取大小,这个方法就是mappingCount。
mappingCount源码
1 | public long mappingCount() { |
containsValue源码
1 | public boolean containsValue(Object value) { |
检查在所有映射(k,v)中只要出现一次及以上的v==value,返回true。需注意:这个方法可能需要一个完全遍历Map,因此比containsKey要慢的多
containsKey源码
1 | public boolean containsKey(Object key) { //直接调用get(int key)方法即可,如果有返回值,则说明是包含key的 |
remove源码
1 | public V remove(Object key) { |
replaceNode源码
1 | final V replaceNode(Object key, V value, Object cv) { |
remove方法的实现思路也比较简单。如下;
先根据key的hash值计算书其在table的位置 i。
检查table[i]是否为空,如果为空,则返回null,否则进行3
在table[i]存储的链表(或树)中开始遍历比对寻找,如果找到节点符合key的,则判断value是否为null来决定是否是更新还是删除该节点。
clear源码
1 | public void clear() { |