ConcurrentHashMap

 

ConcurrentHashMap 如何保证线程安全

https://blog.csdn.net/baidu_28523317/article/details/84262712

jdk1.7

   ConcurrentHashMap将数据分别放到多个slot 中,默认16个,每一个Segment中又包含了多个HashEntry列表数组,对于一个key,需要经过三次hash操作,才能最终定位这个元素的位置,这三次hash分别为:

 

  1、对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);

  2、将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;

  3、将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。

 

  每一个Segment都拥有一个锁,当进行写操作时,只需要锁定一个Segment,而其它Segment中的数据是可以访问的。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
        transient volatile HashEntry<K,V>[] table;
        transient int count;
    }

 

 

jdk1.8

  ConcurrentHashMap在jdk1.8中取消segments字段,直接采用transient volatile Node<K,V>[] table保存数据,采用table数组元素(链表的首个元素或者红黑色的根节点)作为锁,从而实现了对每一行数据进行加锁,减少并发冲突的概率。

 

transient volatile Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        ......
}

 

  ConcurrentHashMap在jdk1.8中将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。jdk1.8中对于链表个数小于等于默认值8的链表使用单向链接,而个数超对默认值的时候将链表转换为红黑树,查询时间复杂度优化到了O(logN)【二分查找】,O(logN)即二叉树的深度。

  ConcurrentHashMap的大部分操作和HashMap是相同的,例如初始化,扩容和链表向红黑树的转变等。但是,在ConcurrentHashMap中,大量使用了U.compareAndSwapXXX。的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代

理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。

  如果slot有头节点,并发 map 使用 Synchronized 锁桶的头节点,保证桶内的写操作是线程安全的(桶内是串行化的)。

  如果 Slot 内是空的(ps:没有头节点,没有数据),这时候,依赖CAS 实现线程安全。

  线程使用 CAS 方式向 slot 里面写头节点数据。

  成功的话,就返回,失败的话,说明有其他线程获取到这个 slot 位置。当前线程只能重新执行写逻辑,再次路由到这个 slot 位置的时候。再次写的时候,桶内的头结点,来保证写线程安全。桶内肯定是串行。

 

ConcurrentHashMap 如何统计当前散列表数据量, 为什么不使用 atomicLong 这种原子类型

  在并发map 里面实际使用 LongAdder ,jdk 8. 

  出于性能的考虑吧,因为 atomic Long 自增操作采用 CAS 实现,CAS 并发小的时候性能还不错,

  但是并发量大的情况下,100 个线程,首先CAS 比较期望值, 如果期望值一致,再执行替换操作,并且CAS 反映到内核层,其实是 cmpxchg 指令,这个指令在执行的时候会检查当前平台是否为多核平台,如果是多核, cmpxchg 会通过锁线程总线的形式保证同一

    时刻只能由一颗 cpu 执行。也就是如果 100 个线程同时执行,反映到平台上仍然是串行通过的。另外如果CAS 操作获取的期望值过期,则后面的线程都会失败。失败之后再去读内存里的最新值作为期望值,再尝试修改。直到成功。这样会浪费CPU 资源。

扩容标识戳

 

LongAdder 怎么解决 Atomic Long 大并发下的性能问题。

  LongAdder遇到热数据,就将热数据拆分开,原来每个请求打到一个点上,现在拆分为几个点,这样冲突概率就小,性能得到提升。利用空间换时间。

 

LongAdder 内部结构

  LongAdder 核心有两个字段

  一个是 Long 类型的 base 字段, 一个是 Cell 数组

  Cell 结构里面有 Long 类型的 value 字段。使用这个Long Adder 过程,如果没有发生并发失败, 数据会全部类加到base, 也是采用 CAS 的方式跟新base 字段,当某个线程与其他线程产生冲突,CAS 修改 base 字段失败的时候。就将cell[] 数据数据构建出来, 再往后累加请求,就不再首先base 字段,根据分配给线程的哈希值,进行一个位于运算。找到对应的一个 cell, 将累加的值 通过 CAS 的方式写入 Cell 里面。 

 

 

 

高16位标识扩容戳,低16位标识扩容的线程数,

ConcurrentHashMap 采用了cas实现无锁并发同步,利用多线程来进行扩容,它时把node数据当作多线程之间的共享任务队列,通过指针来划分每个线程负责的区间,每个线程通过逆序遍历来实现扩容,迁移完的bucket 会被ForwardingNode替换;
1、fwd:这个类是个标识类,用于指向新表用的,其他线程遇到这个类会主动跳过这个类,因 为这个类要么就是扩容迁移正在进行,要么就是已经完成扩容迁移,也就是这个类要保证线 程安全,再进行操作。
2、advance:这个变量是用于提示代码是否进行推进处理,也就是当前桶处理完,处理下一个 桶的标识
3、finishing:这个变量用于提示扩容是否结束用的

https://blog.csdn.net/weixin_42022924/article/details/102865519

https://blog.csdn.net/ZOKEKAI/article/details/90051567

posted @ 2020-10-14 14:11  李荣先辈Java  阅读(158)  评论(0编辑  收藏  举报