Java并发编程之CAS第三篇-CAS的缺点java
经过前两篇的文章介绍,咱们知道了CAS是什么以及查看源码了解CAS原理。那么在多线程并发环境中,的缺点是什么呢?这篇文章咱们就来讨论讨论编程
本篇是《凯哥(凯哥Java:kagejava)并发编程学习》系列之《CAS系列》教程的第三篇:CAS的缺点有哪些?怎么解决。多线程
从源码中(见上图),咱们能够知道do while中的while返回true会一直循环下去(具体分析步骤见上一篇:《Java并发编程之CAS二源码追根溯源》。凯哥(凯哥Java:kaigejava)就不在这里赘述了)。若是并发量不少的话,好比:有十万个线程来并发处理,这这种业务下,不少线程都会修改共享变量,要保证原子性的话,循环会很长时间,假设每一个线程为了保证原子性,循环耗时0.001s的话,那么十万个线程都这么循环下来,对CPU的消耗仍是比较大的。并发
从源码中,咱们知道 Object var1其实就是对象本身。拿上一篇文章举的例子来讲,其实就是atomicInteger本身,也就是共享变量。CAS的do while只能一个this一个this的比较。从这里就能够看出,CAS只能保证一个共享变量的原子性。可是若是用同步锁的话,锁是能够锁对象也能够锁代码块。锁操做的能够不是一个共享变量。学习
先来看看现实生活的例子:this
学校举行运动会,标准操场一圈400米,如今正在进行1200米比赛。1200=400*3.须要跑上三圈。小明和小红比赛,在刚开始的时候,你们都看到小明,小红都在起点,可是小红速度比小明快2/3。这个时候,小明爸爸拿着相机拍摄,在起点时候,拍摄小明,3分钟事后,咱们再来看起点,是小红。7分钟以后,在看起点是小明。难道小红就跑了一圈吗?这固然不对。小红比小明快,当咱们第二次看到小明的时候,小红其实三圈已经跑完了。最终出现的状况就是:小明(小红)小红小明(小红)小明。最终获胜的固然是小红atom
这个例子或者不是很恰当。可是凯哥是想经过这个例子告诉你们,当线程若是出现这种状况的话,会影响到数据结果的。线程
以下图:对象
说明:blog
A线程执行一次耗时:1分钟
B线程执行一次耗时“29.5s
B线程在A线程执行一次的时间内操做主内存的数据变化为:202020192020
当B线程执行2次操做以后,1分钟到了。A线程拿着本身工做区copay的副本值i=2020和主内存的值i=2020。正好相等,这个时候会,主内存的共享变量相对于A线程来讲,是没有变化的。可是其实是有变化的(B线程确实操做过的。如上面举例的,小红已经跑完三圈了。但是小明才跑第二圈呢),若是这个时候在操做,有可能致使数据出问题(赛跑最终结果是小红赢了,而不是小明赢了)。
所谓的ABA就是:在某个监控点的时候数据是A,当过了时间N以后,在监控的时候仍是A。可是在时间N的这段时间内,监控点的数据有可能不是A了,变成过B。这样就更容易理解了吧。
代码说明:
初始的时候,给了变量值为2020.也就是V=2020.如上图1
在通过线程A一顿猛如虎的操做以后,搞出来2020,2021,2020.ABA的效果处理。如上图2.
Sleep了1秒是为了让线程A完成ABA操做的。
而后,线程2在拿着本身副本的变量值A=2020,和主内存V进行比较。发现一致,就更新了2019.
运行结果以下图:
从运行结果来看,线程2也更新成功了。可是,这样是不对的。由于咱们已经知道线程A对共享变量操做过了。那么针对CAS的这些缺点,应该怎么解决呢?欢迎继续学习下一篇。凯哥将介绍三个怎么解决。以及会讲解原子引用、时间戳原子引用两个问题。
解决思路:ConcurrentHashMap(后面凯哥也会详细介绍的)相似的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来。
由于CAS只能一个共享变量一个共享变量的处理。若是想要处理类是代码块或者对象的。可使用同步锁或者是多个变量放到一个对象里面。而后在CAS。由于在JUC包下,有支持对象的原子类,如:AtomicReference(原子引用类)。
在Java中变量的类型分为八大基本类型或八大基本类型的对象类型或者是自定义的对象类型。在并发中,atomicInteger就是基本类型就是int/Integer的原子类。那么自定义的对象怎么实现原子性呢?这就要用到原子引用对象- AtomicReference。
咱们来模拟凯哥心中女神变化过程(注:女神同时只能存在一个,不能存在多个,要保持单一,原子的)。
在X年以前是刘亦菲,X+N年后是林依晨,如今是佟丽娅了。咱们知道,这三个女神都是对象。都有年龄、用户名,是个对象。
建立user对象
她们三个在凯哥心中活动以下:
那么请问在21和23行输入的结果是什么?
编辑
咱们发如今23行依然输出的是林依晨。而不是佟丽娅。为何呢?分析思路见:《Java并发编程之CAS一理解》篇文章的三:cas代码演示部分。
咱们修改以后再来看:
运行结果:
发现心中女神已经更新为佟丽娅了
ABA问题产生的根本缘由是由于:只是线程本身工做空间的变量预期值(副本)和主内存中的值进行了比较。当值相等的时候,就默认没有被其余线程更新过。那么怎么解决这个问题呢?
是否是能够添加一个东西,用来辅助呢?添加一个标记,或者一个版本号,根据版本号+数值来进行判断呢?固然能够了,JDK中也是这么实现的。JDK使用的是时间戳(stamp),而不是咱们说的版本号(version)。咱们来看看时间戳原子引用(AtomicStampedReference<V>).
咱们来看看这个类。
先看构造器:
参数说明:
initialRef:初始值
initialStamp:初始值的时间戳
再来看看CompareAndSet方法:
参数说明:
expectedReference:预期值
newReference:更新值
expectedStamp:预期时间戳值
newStamp:更改后时间戳值
咱们发现这个AtomicStampedReference类和AtomicReference的方法中的区别就是时间戳原子引用类中的方法都添加了预期的时间戳值和修改后的时间戳的值这两个参数。
咱们来看看,使用带有时间戳的原子引用类解决ABA问题的代码:
1:声明共享变量
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(127,1);
(须要说明,若是用数值作demo的话,主要int的取值范围。若是大于127,就会始终返回false。由于 Integer(128) == Integer(128)返回的是false)
线程一先修改执行一个ABA的过程:
编辑
执行完成以后,当前的主内存中版本号应该是3了。
咱们在用线程2来执行compareAndSet:
此时,在线程2中的版本号:tamp应该是1,可是主内存中的版本号已是3了。因此执行后返回false.执行不成功的。
咱们来看看运行结果和咱们预期结果:
运行结果,和咱们预期结果是一致的。说明,添加这个时间戳(版本号)能够解决ABA问题