首页 > 其他分享 >ConcurrentHashMap底层原理

ConcurrentHashMap底层原理

时间:2022-10-11 16:44:59浏览次数:56  
标签:Node ConcurrentHashMap key 链表 tab 原理 null segment 底层

5.6 ConcurrentHashMap底层原理

5.6.1 jdk1.7

5.6.1.1 数组结构

数据结构是数组+segment对象,采用segment分段锁和CAS保证并发。

5.6.1.2 put操作流程

 /**
         * ConcurrentHashMap
         */
        ConcurrentHashMap<String,String> conMap = new ConcurrentHashMap<String, String>();
        conMap.put("k","v");
        System.out.println(conMap.get("k"));

  1. 构造方法

    private static final int DEFAULT_CAPACITY = 16;//segment数组的长度
    private static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子(与扩容有关)
    DEFAULT_CONCURRENCY_LEVEL; //总entry的数目
    
    假设new ConcurrentHashMap(16,0.75f,16)
    说明segment数组大小为16,而每个segment对象内部的数组大小为1(16/16),表示只存一个entry。(默认最低2个)
    
    假设new ConcurrentHashMap(8,0.75f,16)
    说明segment数组大小为16,而每个segment对象内部的数组大小为2(16/16),表示存2个entry。
    
    

    传入的concurrencyLevel
    8---->8
    9---->16
    17---->32
    保证是大于等于concurrencyLevel的2的幂次
    
    
  2. 获取segment对象

    1.第一个if是判断当前位置是否有segment对象,没有则往下执行。
    2.获取segment数组的第一个位置的segment对象的信息。
    3.假设此时有多个线程执行到,这里的if就是判断是否有别的线程执行到了。
    4.各线程生成自己的segment对象。
    5.if之后进入while循环,如果当前位置没有segment对象,则执行cas指令,将自己生成的segment对象s赋给ss大数组的第u个位置。(cas:当第u个位置为null时,处理器会将null更新为seg,处理过程不会呗别的线程中断)
        
    补充:
        CAS指令需要有三个操作数,分别是内存位置V,旧的预期值A,准备设置的新值B。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否者它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上诉的处理过程是一个原子操作,执行期间不会被其他线程中断。
    
  3. 向segment对象里的小数组加入entry对象

1.第一个框:
	trylock():非阻塞加锁
	lock():阻塞加锁
	如果调用trylock()加锁失败,则继续调用scanAndLockForPut()加锁,直到加到锁。scanAndLockForPut()核心代码是while(!trylock()){},主要工作就是一直加锁,直到加到锁。
	
2.第二个框:遍历segment里面的数组的某个位置的链表
	first:获取链表第一个节点
	如果遇到相同的key,则覆盖value,否则生成一个entry对象,加入。

5.6.2 jdk1.8

jdk1.8 的ConcurrentHashMap舍弃了1.7的segment分段锁,采用了CAS+sycronized来并发。
数据结构和1.8的HashMap一样,是数组+链表+红黑树。

5.6.2.1 计算hash值

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS; //HASH_BITS=0x7fffffff
    }
除了高16位和低16位或操作之外,最后还和HASH_BITS相与,其值为0x7fffffff,表示int的最大值01111111...。它的作用主要是使hash值为正数。在ConcurrentHashMap中,Hash值为负数有特别的意义,如-1表示ForwardingNode结点,-2表示TreeBin结点。

5.6.2.2 volatile修饰

使用volatile修饰来保证某个变量内存的改变对其他线程即时可见。可以配合CAS实现不加锁对并发操作的支持。
ConcurrentHashMap的get操作可以无锁,正式由于Node的元素val和指针
next是使用volatile修饰的,在多线程环境下,A线程修改节点val或者新增节点对B线程都是即时可见的,保证了数据的一致性。

5.6.2.3 put操作

public V put(K key, V value) {
    return putVal(key, value, false);
}

    /** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //ConcurrentHashMap 不允许插入null键,HashMap允许插入一个null键
    if (key == null || value == null) throw new NullPointerException();
    //计算key的hash值
    int hash = spread(key.hashCode());//看5.6.1
    int binCount = 0;
    //for循环的作用:因为更新元素是使用CAS机制更新,需要不断的失败重试,直到成功为止。
    for (Node<K,V>[] tab = table;;) {
        // f:链表或红黑二叉树头结点,向链表中添加元素时,需要synchronized获取f的锁。
        Node<K,V> f; int n, i, fh;
        //判断Node[]数组是否初始化,没有则进行初始化操作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //通过hash定位Node[]数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//看注1
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null))) //看注2
                break;                   // no lock when adding to empty bin
        }
        //检查到内部正在移动元素(Node[] 数组扩容)
        else if ((fh = f.hash) == MOVED)
            //帮助它扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //锁住链表或红黑二叉树的头结点,保证并发
            synchronized (f) {
                //判断f是否是链表的头结点
                if (tabAt(tab, i) == f) {
                    //如果fh>=0 是链表节点
                    if (fh >= 0) {
                        binCount = 1;//包括头节点,初始化为1
                        //遍历链表所有节点
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果节点存在,则更新value
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            //不存在则在链表尾部添加新节点。
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //TreeBin是红黑二叉树节点
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        //添加树节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                      value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            
            if (binCount != 0) {
                //如果链表长度已经达到临界值8 就需要把链表转换为树结构
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //将当前ConcurrentHashMap的size数量+1
    addCount(1L, binCount);
    return null;
}

注1:

//返回tab数组的第i个元素
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

注2:

/**
	使用CAS指令,当tab数组的第i个位置是c(null)时,则更新c为v
**/
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

总结:

1.判断Node数组是否初始化,没有则进行初始化。
2.通过hash & (length-1) 定位索引,判断此位置是否有节点,没有则使用CAS加入,用for进行失败重试机制。有则往下执行。
3.判断数组是否在扩容,是则帮助扩容,否则往下执行。
4.锁住头节点,保证并发,判断此位置是链表还是红黑树,如果是链表,则遍历链表,判断是否有相同key的entry对象,有则覆盖value值,否则插入(尾插法)。若是红黑树,则添加一个TreeBin节点。
5.上述操作完成后,加入是在链表中插入的,则还要判断当前链表节点是否大于8,是则要转化为红黑树。

标签:Node,ConcurrentHashMap,key,链表,tab,原理,null,segment,底层
From: https://www.cnblogs.com/lxpblogs/p/16779722.html

相关文章

  • 以太网交换机基本原理
    网络设备调试网络设备调试,我们这里通常讲的都是通过console线接入网络设备进行命令行的调试,当然现在也有很多设备都支持web界面的调试,图形化带来的好处就是可视化,直观,操作简......
  • git工作原理之记录快照而非差异对比
    文件系统,点击进入快照理解,点击进入......
  • STP原理
    1、根桥选举首先比较网桥ID优先级,越小则越优,默认优先级是32768.如果优先级一样,则比较系统MAC地址(非接口MAC地址),MAC地址越小越优先。2、根端口选举①比较BPDU报文中的根......
  • warm-up原理和训练技巧
    原理训练神经网络的一个重要trick是warmup,它被广泛应用在各种模型的训练中。它的命名大概是类比了我们参加体育锻炼前的热身运动。warmup通过操作训练初始阶段的le......
  • 电气原理图制图相关GB标准
    电气技术文件篇GB/T32876-2016《电气信息结构、文件编制和图形符号术语》Terminologyintheareaofinformationstructures,documentationandgraphicalsymbolsGB......
  • 大数据技术之HBase原理与实战归纳分享-中
    @目录底层原理Master架构RegionServer架构Region/Store/StoreFile/Hfile之间的关系写流程写缓存刷写读流程文件合并分区JAVAAPI编程准备示例底层原理Master架构Meta......
  • 斗鱼 H5 直播原理解析,它是如何省了 80% 的 CDN 流量?
    斗鱼直播相信大家都听说过,打开斗鱼官网就可以直接在浏览器中观看直播。那么斗鱼是如何实现浏览器视频直播的呢?本篇文章就来解析斗鱼是如何实现直播的,以及它是如何节省80%......
  • 说说Nodejs高并发的原理
    写在前面我们先来看几个常见的说法nodejs是单线程+非阻塞I/O模型nodejs适合高并发nodejs适合I/O密集型应用,不适合CPU密集型应用在具体分析这几个说法是不是、为什......
  • webpack模块化的原理
    commonjs在webpack中既可以书写commonjs模块也可以书写es模块,而且不用考虑浏览器的兼容性问题,我们来分析一下原理。首先搞清楚commonjs模块化的处理方式,简单配置一下webp......
  • AcWing算法提高课 容斥原理
    容斥原理的复杂度是2^n,一般n不会很大形如:  由于容斥原理一共有2^n中选法,可以用二进制枚举,1表示选择某个条件。然后将偶数个1的状态加起来,奇数个1的状态减去例题:ht......