内容提要
• 如何引出该话题
• 通过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. 同时给出了引出该值钱话题的说辞;