生动有趣带你看JDK1.8-ConcurrentHashMap的put源码

上一节说到hashmap的源码,那么这一节来说说ConcurrentHashMap的源码,因为ConcurrentHashMap是线程安全版多的hashmap,所以看这节最好先看hashmap源码那一节,所以在这里给你们准备好了传送门

生动有趣带你看JDK1.8-HashMap的put源码,看完直呼原来这么简单!

HashMap在多线程的情况下操作可能会和单线程操作情况数据不一致,就会出现所谓的线程安全的问题,线程安全就会想到的做法是加锁,也就是HashTable,但是加锁性能太差了,为什么这样说呢,HashTable直接把锁加在了put方法上,一个操作只能让一个线程进入,必然性能变差。

那采用无锁化呢?就衍生出了ConcurrentHashMap,ConcurrentHashMap采用CAS,那么什么是CAS呢?

CAS:保证对某个线程安全,获取对操作对象的新值和你需要修改的值,两者进行比较,如果相等就证明是安全的,如果不相等就证明被其他线程改过了,那么你就不能进行改动。

首先套路还是一样,先缕清成员变量,其实是和HashMap一样的数据结构,都是需要有数组的,但是不同的是定义的时候加了volatile可见性这样的单词,代表这个成员变量是要可见的,因为是要进行多线程操作了麽

1. 数组初始化

那么看源码之前想一下HashMap的操作哪里不安全?数组初始化是否安全、存储数据是否安全?

假设多线程模式不加锁,T1线程进来进行了初始化要存储数据了,T2此时也进来了然后进行了初始化操作,那么就乱了,到底初始化还是存储数据,容易出现数据混乱的问题,所以数组初始化是要保证安全的,这样一想存储数据就一定需要加锁。

那源码来看一下是不是这样的操作,还是熟悉的putVal方法,刚进来tab一定是null,所以进入初始化。

进入initTable初始化方法,映入眼帘的是一个判断,sc是局部变量默认为0,那么sizeCtl是在哪里定义的呢?(查看下图第二个)原来也是定义的成员变量,那么第一次sizeCtl是等于0的,把0赋值给了sc,判断以后一定不小于0。

不小于0就会进入另一个判断,发现这里加了CAS了,关键性的一幕来了U.compareAndSwapInt(this, SIZECTL, sc, -1)它来了,这句话的意思判断SIZECTL和sc是否相等,那么看下SIZECTL,如下图(第二个)定义的,发现也是定义的变量没有进行赋值默认为0,所以SIZECTL=0,sc=0说明数一致可以进入方法,进行数组初始化了。那么数不一致sc被赋值成-1,(sc=sizeCtl)<0就成立了,线程就会作出让步Thread.yield()。

所以ConcurrentHashMap对数组的初始化使用了CAS无锁化的方式保证了线程安全。

2.存储数据

那除了这块需要加上同步还有哪里需要加上呢,是不是put存储数据的时候也需要加上,进入putVal方法看源码,初始化过后进行hash计算以后,那么又进行了CAS的判断在第一次进入的时候i是为null,判断当i和null的比对以后才可以创建节点。

初始化完成,也能创建节点并且都能保证线程安全。那么接下来就是进入节点是有值的要么进行链表操作,要么进行红黑树操作,要么Key值相等,进行put更新操作

下图是Key值相等,进行put更新

判断下一个节点是否有值,next没值进行存储,链表存储操作

树结构存储的操作

那么以上这些存储节点的操作是不是需要加锁呢?答案是要的,需要注意的是这里不使用CAS了,这里就变成了synchronized,为什么这里使用了synchronized而不用CAS了呢,因为CAS需要进行比对,节点太多一个一个比对比较麻烦,而这里用了synchronized,是因为巧妙的用了synchronized的作用域,可以看一下synchronized方法里传递的f是什么,f就是当前的节点,也就是说作用域就是锁住当前节点,其他put操作算出来的hash只要不是数组同一个位置就不会进行锁操作,主要减少锁的粒度,就增加了性能。

3.数据扩容

那么以上都讲完了,是不是还缺个功能,对的就是扩容了,先说一下扩容需不需要线程安全,答案是一定的,我第一个线程在搬数据,第二个也在般数据,那么不就是乱套了么,还搬的是一个。

那么这里说一下,这里扩容不是锁住,它会让其他线程帮助那个正在搬运的线程,也就是说当t1线程正在扩容,t2线程进来就不能put了,而是领取任务帮忙搬运。怎么个搬运法呢,假如扩容64个长度,那么t1正在搬运前16个,那么就把另一个长度16搬运的工作交给T2,来都来了别闲着哈哈哈,T3线程进来发现还没扩容完继续帮忙领取任务再给16长度的,直到都扩容完成!

 

先看一下扩容源码f.hash头节点==MOVED,MOVED就是下图默认给-1,这里你只需要知道,达到扩容的条件就会给hash赋值为-1

做一些复杂的操作方法,最后有条件就进行扩容,那么这里就是用了addCount方法判断是否扩容,当然addCount方法里的代码有点多,但是主干就是达到条件就会去扩容,然后都会去transfer方法。

进入transfer方法,大家都会是先领取任务,stride是步伐的意思,MIN_TRANSFER_STRIDE全局变量是多少呢?下图MIN_TRANSFER_STRIDE是16,也就是判断当前数组小于16个,如果小于就不用其他线程去帮忙搬了,我一个人就行了。

如果数组大小是64个,我需要put元素,但是发现其他的线程在扩容,那么我此刻就是需要去帮忙搬元素就进入helpTransfer方法

helpTransfer方法里也是进行一系列的判断,最终还是进入transfer方法里进行帮忙,进入到transfer以后就要去领取第二个阶段的16个数组大小的任务,而真正核心的扩容步骤是和HashMap是一样的。

你会发现transfer方法的源码,前部分是进行一个线程任务领取

transfer方法的源码,中间部分是不断的判断自己的任务到底是否完成,如果完成做个标记,如果全部完成就统统返回

后部就是搬运数据的过程

那么除了加锁啊,cas判断啊,在存储数据操作其实是和hashMap是一样的。

那么ConcurrentHashMap主要的部分就说完了,本博客是学习咕泡学院的-jack老师的公开课讲解的进行整理总结,码字不易,给个赞再走被!希望本博客能够帮助你!

加下关注更感激不尽了,我会持续奋进学习书籍、视频,然后记录博客,让大家都能一起学习到!笔心

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值