难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJIDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在 一章中介绍了 Java多 线程 基础知识总结及学习资料 。
在 一章中介绍了 内存可见性volatile关键字 。
在 一章中介绍了 什么是原子性 。
现在介绍 比较并交换CAS技术 。
2.什么是 比较并交换CAS技术 ?
CAS全文叫“ compare and swap ”。
- compare:比较。
- and:并。
- swap:交换。
故, “compare and swap”简称比较并交换技术,也叫CAS技术。
CAS中的“ 比较 ”是谁跟谁比?“ 交换 ”又是谁和谁交换?
要解决上述两个问题,还得说说为什么需要CAS。
3.为什么需要CAS技术?
结合例子来说这个问题,例子很简单: 从0开始依次递增 。
将上述问题用程序描述出来。
我们也不计算那么多,就从0递增到1即可。
首先,创建一个计数器:
然后,将递增任务创建出来:
接着,在run()方法里面先输出未递增之前的count:
然后,递增count:
接着,输出count递增后的新值:
run()方法书写完毕。
最后,创建线程并将递增任务传递给线程,随后启动线程:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。从0递增到1。
现在有一个问题,由于有多个线程在操作count变量,所以出现旧值是0,新值却是6的情况。
来一个实际例子说明问题吧。
还是刚刚的例子, 在启动线程的后面将count设置为5 :
再来看看run()方法里面有什么需要改动的。
运行程序,执行结果:
静图:
从运行结果来看,符合预期。 主线程如期干扰了其它线程正在使用中的变量 。
出现了上述问题,该如何解决呢?
有小伙伴提出用volatile关键字解决。
那么,volatile关键字到底能不能解决这个问题呢?
下面,我们就来看看。
4.volatile关键字不能解决多线程修改变量的问题
还是上一小节的例子,这里只需改动一处即可。
将count变量用volatile关键字修饰一下:
例子改写完毕。
运行程序,执行结果:
静图:
从运行结果来看,不符合预期。我们的问题还没有解决。
既然volatile关键字不能解决上述问题,那么同步能不能解决呢?
下面,我们再来看看同步。
5.同步不能解决 多线程修改变量的问题
还是上一小节例子,我们只需将run()方法里面代码同步一下即可。
这里无论是使用隐式锁 synchronized 还是显式锁Lock都可以。
在此例子中,我们采用的是隐式锁synchronized:
最后,我们将用于修饰count的关键字volatile移除掉:
例子改写完毕。
运行程序,执行结果:
静图:
从运行结果来看,不符合预期。同步解决不了此类问题。
那怎么办呢?
接下来,介绍一个解决此类问题的技术:CAS。
6.CAS技术
按照专业一点的术语描述CAS就是:
我们来对上述说明分句解释+举例说明。
第一句:CAS操作包含三个操作数——内存值、预期原值和新值。
“内存值”指的是变量在内存中的值。
比如说count,它被创建在内存中,它的值是0:
“预期原值”指的是你认为这个变量在内存的值应该是什么。
比如说,我认为count在内存中的值应该是0。
“新值”指的是新赋给变量的值。
比如说,你要将count递增之后的值赋给count,那么count递增之后的值就是新值。
第二句:如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。
这一句很好理解,就是 count在内存中有一个值,然后你猜猜它会是几,如果你猜对了,你可以给count赋一个新值。即预期原值 = 内存值时,将新值赋给内存中的变量。
然后你说:“我觉得count=0。”
此时,你猜对了,那么你就可以给coun赋上新值,比如你把count+1后的值赋给count。
待会程序会演示这块内容。
第三句:否则,处理器不做任何操作。
这一句也很好理解,就是 你猜错了的话,什么都不做。即预期原值 != 内存值时,什么都不做。
例如,你预测count=0,但实际上count=5,因为0 != 5,所以程序应该什么操作也不做。
第四句: 无论哪种情况,它都会在CAS指令之前返回该位置的值。
这一句就是在写CAS方法时需要体现的,该句的意思就是方法需要返回变量的原值,就是我们最开始获取到的那个值。
不明白没关系,待会结合例子看就清楚了。
我们来按照以上的描述修改例子。
还是上一小节的例子,我们先获取到内存值:
然后,输出旧值:
接着,判断预期原值和内存值是否相等,这里预期原值我们给一个0好了:
最后,当预期原值和内存值相等时,我们才赋给count新值,即count递增之后再赋给count:
例子改写完毕。
运行程序,执行结果:
静图:
从运行结果来看,符合预期。这下问题真的是解决了。
何以见得?
输出语句只有旧值,没有新值。
说明什么?
说明内存值被其他线程改动过,导致和我的预期原值不同,所以程序什么也不做,即不赋新值。
接下来,我们将这个程序写得更完整些。不在Main类里面把事情都做完。
顺便把CAS算法模拟出来。
7.模拟CAS算法
首先,创建出CompareAndSwap类:
然后,在类中定义一个存储值的变量:
接着,定义一个获取该变量的方法, 因为涉及到多线程操作,所以将方法设置为同步方法,用synchronized关键字修饰 :
然后,我们写上比较并交换的方法, 该方法需要两个参数,一个是预期原值expectedValue,一个是新值newValue :
接着,在compareAndSwap(int expectedValue, int newValue)方法里面获取原值并返回:
这里返回原值的规则是“ 无论哪种情况,它都会在CAS指令之前返回该位置的值。 ”这句话所说的。
接下来,在比较预期原值和原值是否相等:
如果相等,则把新值赋给变量:
好了,compareAndSwap(int expectedValue, int newValue)方法书写完毕。
CompareAndSwap类也书写完毕。
接下来,我们去试试CompareAndSwap类。
回到Main类中,将之前写的内容全部删掉。
然后,创建出CompareAndSwap实例:
接着,创建出计算任务:
然后,在run()方法里面先获取变量的值,我们将获取到的值作为预期原值:
接着, 调用cas对象的compareAndSwap(int expectedValue, int newValue)方法,预期原值就是上面获取的变量值,新值就是将递增获取到的变量值 :
然后,输出compareAndSwap(int expectedValue, int newValue)方法返回值:
好了,run()方法书写完毕。
接下来,我们创建多个线程执行计算任务,用于模拟多线程修改变量:
最后,在计算任务整体完成之后输出最新变量的值:
不过,我们延时1秒钟再输出,想等前面多线程计算完成之后再输出:
例子书写完毕。
运行程序,执行结果:
静图:
从运行结果来看,符合预期。从0递增到10准确无误。
如果我想看看新值是否设置成功,该如何做呢?
我们可以在CompareAndSwap类中添加一个返回新值是否设置成功的方法:
在compareAndSet(int expectedValue, int newValue)方法内部只需调用compareAndSwap(int expectedValue, int newValue)方法,然后比较预期原值和compareAndSwap(int expectedValue, int newValue)方法返回值是否相等,若相等则说明新值设置成功:
回到Main类。
我们 将“cas.compareAndSwap()”改为“cas.compareAndSet()” :
然后,输出返回值:
例子改写完毕。
运行程序,执行结果:
静图:
从运行结果来看,符合预期。新值的设置都成功了,最后输出的结果也是和我们之前的结果一样。
那什么时候新值会设置失败呢?
这里我们来演示一下。
还是刚刚的例子。
调用compareAndSet(int expectedValue, int newValue)方法时,第一个参数传递一个非原值的数即可,随便写什么值都行:
例子改写完毕。
运行程序,执行结果:
静图:
从运行结果来看,符合预期。我们的新值除了第一个设置成功以外,其他都是false。
因为第一个原值就是0,我们给的也是0,所以第一个新值设置成功了。
最后的结果也说明了我们新值只有第一个被设置成功。
Java原子操作的第一章、第二章以及第三章将介绍原子操作预备知识,大家要好好理解方可在后面的学习中事半功倍。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:
总结
- CAS全文叫“compare and swap”。
- compare:比较。and:并。swap:交换。故,“compare and swap”简称比较并交换技术,也叫CAS技术。
- CAS操作包含三个操作数——内存值、预期原值和新值。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。
至此,Java中CAS技术相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
下一章
“全栈2019”Java原子操作第四章:AtomicBoolean介绍与使用
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!