8,用ConcurrentHashmap综合展示并发、集合和读源码的能力

内容提要
• 如何引出该话题

• 通过put和get源码,了解ConcurrentHashMap的底层结构 和hash流程;

• 以CAS+ Synchronized管理并发的方式;

• 对比jdk1.7和1.8的差别,介绍ConcurrentHashMap; 

• 以实例介绍volatile的含义;

 

引出话题
• 谈到HashMap和线程不安全对象时可以引出;

• 为什么要引出该话题?包含值钱要素太多;

• 回答数据结构hash表、红黑树等相关问题时可以引出;

• 谈到并发和volatile时可以引出;

HashMap和SynchronizedMap的差别

• JDK1.7里,以“锁”segment的方式保证并发 ;

• HashMap线程不安全;

• SynchronizedMap的put和get封装了HashMap相关方法, 并通过互斥锁保证线程安全 ;

• JDK1.8里,以“CAS” 的方式保证并发;

• ConcurrentHashMap做put时,用CAS+Synchronized 保证线程安全,更轻量;

JDK1.7的实现方式
• 读时不加锁,写时锁segment ;

• segment数组->HashEntry数组->HashEntry列表;

• 两次hash,一次定位到segment,一次定位到HashEntry头部;



JDK1.8的实现方式

. 冲突会产生链表,链表数大于8,以红黑树方式存储;

• 以node数组加列表或红黑树的方式实现;

• 遍历链表时间复杂度是O(n),遍历红黑树是O(logN),利于冲 突数大的场景;


Node

首先是Node数组,里面是node对象。实现了 Map.Entry<K,V>数组,是个键值对。里面的 val  和 next 都是volatile类型的!

也就是说,在扩容的时候会发生变化,用volatile,保证内存间的可见性。,一个线程在扩容的时候,当前再操作其它的线程也可以看到扩容的变化!从中我们直接举例volatile的用法。


• val和next都会在扩容时发生变化,所以加上volatile来保持可见性 和禁止重排序;

 • 用next指向下一个Node,以处理冲突; 



JDK1.8里put的实现细节(含CAS原理)

• 如果Node数组为空,则调用initTable方法初始化Node数组;

• 计算key的hash值,并定位到Node里的对应位置 ;

• 如果当前Node位置为空,即无冲突,则以CAS方式插入;

• 多个线程尝试使用CAS同时更新同一个变量时,只有其中一个 线程能更新变量的值,而其它线程都失败,失败的线程并不会 被挂起,而是被告知这次竞争中失败,并可以再次尝试。;

 

详细的描述是:

调putVal方法来实现的,在其中呢 看到 如果tab 也就是node数组为空的时候,它会调initTable方法去初始化一个数组,此时的noe数组为空,也就是第一个插入,不存在冲突的时候,此时调casTabAt方法,以cas方法插入,如果里面已经有一个或者多个元素的时候。也就是针对同一个hash值,它已经产生冲突时 ,通过Synocined的方法插入,我们就找到当前node,插入到l链表最后一位!

    由此呢 ,我们可以看到,如果我们node没有初始化就初始化,如果当前数组没有值,就说明没有冲突,可以直接以cas方式去插入!,如果已经有值有冲突的时候,我们就以Synocined得方法插入。。

 

 

 


CAS的讲解


• Compare And Swap,CAS(V,E,N),如V等于E,则将V设为N。若V 和E不等,说明已有其他线程做了更新,当前线程什么都不做,或更改 V,E和N参数再重试。;

• put里的casTabAt方法,实际调用compareAndSwapObject;

• 比较当前tab里的i号索引是否为null,是则插入Node<K,V>,不是, 则说明有其它线程已更改,不做操作;

如果有多线程同时更新时候,其中只有一个线程更新成功,更新成功后 对于其他线程来说 V和E一定不可能相等。这样的话  保证了一个线程更新成功 其他线程更新失败。但是在更新失败的时候,此时这个线程是不会卡住的,是不会阻塞状态的,它会更新参数,下次再去CAS,

compareAndSwapObject的方法其实就是比较当前tab里的i号索引是否为null,是则插入Node<K,V>,不是, 则说明有其它线程已更改,不做操作;

JDK1.8里put的实现细节

• 如果当前Node不为空,对该Node加synchronized锁,并加入到 该Node所指向的链表里;

• 如当前Node里包含的链表节点数大于8,则用treeifyBin方法 把链表转红黑树;

• 如当前Node已经用红黑树存储数据,则通过putTreeVal方法 插入新的键值对;

 JDK1.8里get方法的实现细节
• 计算key的hash值,如果Node数组里匹配到首结点即返回;

• 否则调用next方法遍历链表或红黑树,由于可能会有冲突,需调 用equals方法;

get方法做法是:,

先通过key去计算hash值,调equals方法,判断hash一致 那就直接返回如果Node数组里匹配到首结点即返回 。如果不相等那就说明有冲突,没冲突直接返回  有冲突就 如果不相等 则调用next方法去遍历链表或者红黑树。由于可能会有冲突,在遍历的时,依次遍历每个元素,调用equals方法  得到相等元素再返回 ,如果得不到的话就返回null;

看get方法的源码,它其中既没有加锁 ,也没有任何关于并发的操作,是因为读方法是可以并发的,不需要额外的操作

 


volatile的实例

• 容纳Node的数组时volatile类型;


• 表示长度的变量也是volatile类型;

 

• 多个线程访问volatile变量时,用的是最新值;

总结
1. 通过底层源码,展示了ConcurrentHashMap的数据结构;

2. 通过put方法,展示了CAS等并发能力 ;

3. 通过put方法,引出了红黑树话题 ;

4. 通过get方法,进一步展示了hash表和冲突相关的技能;

5. 通过基于ConcurrentHashMap的实例,展示了volatile的相关技能 ;

6. 同时给出了引出该值钱话题的说辞;
 


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值