jdk 1.8版本
ConcurrentHashMap在1.8中的实现,相比于1.7的版本基本上全部都变掉了。首先,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。而对于锁的粒度,调整为对每个数组元素加锁(Node)。jkd 1.7版本的源码解读感兴趣的可以看这篇文章:ConcurrentHashMap原理分析
接下来主要对jdk 1.8版本进行分析
1、首先看一下类的属性
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable { private static final long serialVersionUID = 7249069246763182397L; // 表的最大容量 private static final int MAXIMUM_CAPACITY = 1 << 30; // 默认表的大小 private static final int DEFAULT_CAPACITY = 16; // 最大数组大小 static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 默认并发数 private static final int DEFAULT_CONCURRENCY_LEVEL = 16; // 装载因子 private static final float LOAD_FACTOR = 0.75f; // 转化为红黑树的阈值 static final int TREEIFY_THRESHOLD = 8; // 由红黑树转化为链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; // 转化为红黑树的表的最小容量 static final int MIN_TREEIFY_CAPACITY = 64; // 每次进行转移的最小值 private static final int MIN_TRANSFER_STRIDE = 16; // 生成sizeCtl所使用的bit位数 private static int RESIZE_STAMP_BITS = 16; // 进行扩容所允许的最大线程数 private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // 记录sizeCtl中的大小所需要进行的偏移位数 private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 一系列的标识 static final int MOVED = -1; // hash for forwarding nodes static final int TREEBIN = -2; // hash for roots of trees static final int RESERVED = -3; // hash for transient reservations static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash // 获取可用的CPU个数 static final int NCPU = Runtime.getRuntime().availableProcessors();// 表 transient volatile Node<K,V>[] table; // 下一个表 private transient volatile Node<K,V>[] nextTable; // 基本计数 private transient volatile long baseCount; // hash表初始化或扩容时的一个控制位标识量 // 负数代表正在进行初始化或扩容操作 // -1代表正在初始化 // -N 表示有N-1个线程正在进行扩容操作 // 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小 private transient volatile int sizeCtl; // 扩容下另一个表的索引 private transient volatile int transferIndex; // 旋转锁 private transient volatile int cellsBusy; // counterCell表 private transient volatile CounterCell[] counterCells;// 视图 private transient KeySetView<K,V> keySet; private transient ValuesView<K,V> values; private transient EntrySetView<K,V> entrySet; } static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; }
2、put方法解析
put的步骤大致如下:
① 判断存储的key、value是否为空,若为空,则抛出异常,否则,进入步骤②
② 计算key的hash值,随后进入无限循环,该无限循环可以确保成功插入数据,若table表为空或者长度为0,则初始化table表,否则,进入步骤③
③ 根据key的hash值取出table表中的结点元素,若取出的结点为空(该桶为空),则使用CAS将key、value、hash值生成的结点放入桶中。否则,进入步骤④
④ 若该结点的的hash值为MOVED,则对该桶中的结点进行转移,否则,进入步骤⑤
⑤ 对桶中的第一个结点(即table表中的结点)进行加锁,对该桶进行遍历,桶中的结点的hash值与key值与给定的hash值和key值相等,则根据标识选择是否进行更新操作(用给定的value值
替换该结点的value值),若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点(如果已经是红黑树,则插入树中)。
这一步通过synchronized对首节点加锁,保证了线程安全。进入步骤⑥
⑥ 若binCount值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储。对桶中第一个结点进行加锁synchronized,保证线程安全
⑦ 增加bincount的值,扩容
在putVal函数中会涉及到如下几个函数:initTable、tabAt、casTabAt、helpTransfer、putTreeVal、treeifyBin、addCount函数。
2.1、initTable
没有使用加锁保证线程安全,而是使用了cas替代了加锁。
每个线程都会通过对属性sizeCtl进行cas操作,成功的线程执行初始化操作,返回初始化后的 Node<K,V>[] tab。保证了线程安全
对于table的大小,会根据sizeCtl的值进行设置,如果没有设置szieCtl的值,那么默认生成的table大小为16,否则,会根据sizeCtl的大小设置table大小。
2.2、casTabAt
使用CAS进行设置第一个节点,并发不高,使用CAS比锁要更好
2.3、helpTransfer
用于在扩容时将table表中的结点转移到nextTable中。
2.4、putTreeVal
用于将指定的hash、key、value值添加到红黑树中,若已经添加了,则返回null,否则返回该结点
2.5、treeifyBin
超过阈值,转换为红黑树结果。转换过程中,对头结点加锁。线程安全
2.6、addCount
更新baseCount的值,
检测是否需要进行扩容,
如果已经有其他线程在执行扩容操作,帮助扩容
2.7、transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
- 计算每个线程可以处理的桶区间。默认 16.
- 初始化临时变量 nextTable,扩容 2 倍。
- 死循环,计算下标。完成总体判断。
- 如果桶内有数据,同步转移数据。通常会像链表拆成 2 份。通过对节点加锁保证线程安全,避免putVal 的时候向链表插入数据
3、get方法
get()方法没有加锁操作,步骤如下:
- 首先定位到table[]中的i。
- 若table[i]存在,则继续查找。
- 首先比较链表头部,如果是则返回。
- 然后如果为红黑树,查找树。
- 如果不是红黑树,循环链表查找。
参考
https://www.cnblogs.com/banjinbaijiu/p/9147434.html