ConcurrentHashMap源码解析
简介
ConcurrentHashMap是Java SDK提供的线程安全的HashMap类。在多线程并发的场景下,用它做添加、删除是不会出现线程安全问题的。
它实现了ConcurrentMap和Serializable接口。
实现逻辑
ConcurrentHashMap底层的实现是Node + 散列表+红黑树。
主要属性
- DEFAULT_CAPACITY:ConcurrentHashMap的默认大小是16
- LOAD_FACTOR:负载因子。ConcurrentHashMap在超过16 * 0.75之后,就需要扩容了。
- TREEIFY_THRESHOLD:发生哈希冲突的链表长度如果大于等于8,就会转变为红黑树
- UNTREEIFY_THRESHOLD:发生哈希冲突的红黑树,在小于等于6个元素之后,就会回退成链表
- MIN_TREEIFY_CAPACITY:当ConcurrentHashMap的大小大于等于64的时候,才允许将冲突的链表转为红黑树
- Node<K,V>[] table 保存哈希桶的数组,大小是2的倍数
- CounterCell[] counterCells:保存各个桶大小
主要函数
构造函数
无参构造函数
创建一个空map,默认大小为16
1 | public ConcurrentHashMap() { |
有参构造函数(指定初始化大小)
初始化时候指定Map大小,然后会初始化为比指定大小最近的一个2的幂次数值。比如,指定大小为12的话,就初始化为16,为17的话,就初始化为32.
1 | public ConcurrentHashMap(int initialCapacity) { |
有参构造函数(指定map)
1 | public ConcurrentHashMap(Map<? extends K, ? extends V> m) { |
有参构造函数(指定初始化大小和负载因子)
调用的是下面的一个构造函数
1 | public ConcurrentHashMap(int initialCapacity, float loadFactor) { |
有参构造函数(指定初始化大小、负载因子以及并发度)
需要注意的是,map的桶个数至少是要和并发度相等的。即并发度为N,那么桶的数目就必须大于等于N
1 | public ConcurrentHashMap(int initialCapacity, |
size()
size方法实现很简单,就是调用sumCount方法,然后判断下是否溢出。
1 | public int size() { |
sumCount()
先看下sumCount的实现
1 | final long sumCount() { |
sumCount的逻辑也比较简单,就是使用了baseCount这个变量和CounterCell数组。
baseCount和CounterCell的具体使用方式,我们在put方法里面在详细说明吧。
先简单看下baseCount变量
baseCount
baseCount变量是主要用在没有竞争场景下计数使用的。通过CAS修改。
CounterCell数组
在并发的场景下,如果CAS更新baseCount失败了,那么失败的线程就会创建CounterCell对象,用来保存部分总数。
isEmpty()
判断下sumCount的返回是否为0即可。
1 | public boolean isEmpty() { |
get(Object)
先看源码
1 | public V get(Object key) { |
get方法的流程如下:
- 首先通过spread方法,计算出key的hash值
- 如果Node数组不为空,并且有值,判断头结点的key是不是和给定的Key相等
- 如果头结点的key和给定的key相等,直接返回头结点的值
- 否则,判断当前节点是树节点还是链表节点
- 树节点通过find方法寻找
- 链表节点就遍历寻找
spread方法
1 | static final int spread(int h) { |
此方法将key的哈希值的低16位和高16位做异或运算
containsKey(Object)
基于上文get方法的实现,判断get出来的值是否为null
1 | public boolean containsKey(Object key) { |
put(Object, Object)
调用了内部方法putVal
1 | public V put(K key, V value) { |
putVal方法
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
总的来说,put方法的步骤是:
- 判断table是否为空,是的话,就先初始化table
- 如果对应位置的桶是空的,就新建一个Node节点CAS插入
- 如果要插入的桶的Hash值为Moved,也就是-1,说明其他线程正在扩容,进入协助扩容方法
- 插入链表或者是树
- 如果完成之后,如果链表个数超过阈值,转换为树
- map的大小加1
initTable方法
1 | private final Node<K,V>[] initTable() { |
initTable在执行的时候,发现有其他线程也在做初始化动作的话,它会调用Thread.yield方法,释放cpu,让其他线程继续做初始化动作。
helpTransfer方法
1 | final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { |
addCount方法
1 | private final void addCount(long x, int check) { |