【Java并发编程】23、ConcurrentHashMap原理分析(1.7和1.8版本对比)

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()方法没有加锁操作,步骤如下:

    1. 首先定位到table[]中的i。
    2. 若table[i]存在,则继续查找。
    3. 首先比较链表头部,如果是则返回。
    4. 然后如果为红黑树,查找树。
    5. 如果不是红黑树,循环链表查找。

参考

ConcurrentHashMap的JDK1.8实现

https://www.cnblogs.com/banjinbaijiu/p/9147434.html

https://www.jianshu.com/p/749d1b8db066

http://www.importnew.com/22007.html

原文地址:https://www.cnblogs.com/wangzhongqiu/p/8981824.html