ConcurrentHashMap的实现原理

并发环境下为什么使用ConcurrentHashMap

1. HashMap在高并发的环境下,执行put操作会导致HashMap的Entry链表形成环形数据结构,从而导致Entry的next节点始终不为空,因此产生死循环获取Entry

2. HashTable虽然是线程安全的,但是效率低下,当一个线程访问HashTable的同步方法时,其他线程如果也访问HashTable的同步方法,那么会进入阻塞或者轮训状态。

3. 在jdk1.6中ConcurrentHashMap使用锁分段技术提高并发访问效率。首先将数据分成一段一段地存储,然后给每一段数据配一个锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问。然而在jdk1.8中的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,底层依然采用数组+链表+红黑树的存储结构。

JDK1.6分析

ConcurrentHashMap采用 分段锁的机制,实现并发的更新操作,底层由Segment数组和HashEntry数组组成。Segment继承ReentrantLock用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶。HashEntry 用来封装映射表的键 / 值对;每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组,下面我们通过一个图来演示一下 ConcurrentHashMap 的结构:

 

JDK1.8分析

改进一:取消segments字段,直接采用transient volatile HashEntry<K,V> table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

 ConcurrentHashMap初始化

首先会创建segment数组,长度为默认(16)或传入的并发值的大于等于的2的次方数,不可扩容
初始化(s0)segment对象并保存,并初始化他的属性,创建一个entry数组,entry数组初始化长度为 (容器长度 / segment数组长度)大于等于2的次方,在下一次进行添加数据的时候,会直接取这个s0对象
这里初始化了他的segment[0]

ConcurrentHashMap如何存取数据的

1.7:
    put():
        1.先计算出key值的hash值,然后通过hash值(对segment[]进行取余)找到数组中对应的segment对象
        2.尝试获取锁,失败则自旋保证成功。
        3.获取锁,然后通过计算出的hash值(对hashentry[]进行取余)找出对应的entry对象
            遍历链表中,查找有没有相同key值对象
            有: 旧值覆盖新值
            没有: 添加到链表中(1.7/头部  |  1.8/尾部)
    get():
        1.通过key值的hash值定位到对应segment对象,再通过hash值定位到具体的entry对象
        2.遍历链表,通过equals取出数据
        3.由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的
            保证了内存可见性,所以每次获取时都是最新值
        4.整个过程不需要加锁

1.8:
    put():
        1.根据 key 计算出 hashcode
        2.判断是否需要进行初始化。
        3.即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据
        4.利用 CAS 尝试写入,失败则自旋保证成功。如果都不满足,则利用 synchronized 锁写入数据
        5.如果数量大于 树形化阈值 则要转换为红黑树。
    get():
        1.根据计算出来的 hashcode 寻找segment,如果就在桶上那么直接返回值。
        2.如果是红黑树那就按照树的方式获取值。
        3.就不满足那就按照链表的方式遍历获取值。
 实现有何不同?为什么这么做?

线程同上.

数据结构:
    1.7: 数组 + 链表
    1.8: 
        1.7的底层还是链表,在查询数据的时候需要遍历,导致效率很低
        1.8和HashMap比较像,也引入了红黑树,把值和next采用了volatile去修饰,保证了可见性

 volatile的特性是啥?

volatile是Java提供的一种轻量级的同步机制:
    1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值
        这新值对其他线程来说是立即可见的。(实现可见性)
    2.禁止进行指令重排序。(实现有序性)
    3.volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

ConcurrentHashMap 线程安全怎么做的?

1.7:
    采用了分段锁的机制,当一个线程占用锁时,不会影响到其他的Segment对象
1.8:
    抛弃了原来的分段锁,采用了 CAS 和 synchronized 来保证并发的安全

CAS是啥?ABA是啥?场景有哪些,怎么解决?

CAS:
    是乐观锁的一种实现方式,是一种轻量级锁
    线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,
    若未被其他线程修改则写回,若已被修改,则重新执行读取流程
ABA:
    当线程对值的修改过程中,另一个线程也对这个值进行了修改,并把它改为了原来的值
        此时,这个值已经不是原的值了
    解决方案:
        版本号 / 时间戳
 

synchronized锁升级策略

    1.先使用 偏向锁 优先同一线程然后再次获取锁
    2.如果失败,就升级为 CAS 轻量级锁,失败就会短暂自旋,防止线程被系统挂起。
    3.最后如果以上都失败就升级为重量级锁。
 

 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值