zoukankan      html  css  js  c++  java
  • 【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

  • 相关阅读:
    c# Action,Func,Predicate委托
    c# 匿名方法
    IIS网站无法启动,提示 另一个程序正在使用此文件
    c# Http下载
    mciSendString详解(转)
    【NOIP2006PJ】数列(sequence)题解
    2020.04.29【NOIP普及组】模拟赛C组30总结
    【USACO 2019 December Silver】Milk Visits题解
    【USACO 2019 February Bronze】Measuring Traffic 题解
    【USACO 2019 February Bronze】Measuring Traffic 题解
  • 原文地址:https://www.cnblogs.com/wangzhongqiu/p/8981824.html
Copyright © 2011-2022 走看看