CAS原理以及CAS带来的三大问题
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CAS原理以及CAS带来的三大问题相关的知识,希望对你有一定的参考价值。
参考技术A 参考: https://www.jianshu.com/p/ab2c8fce878b
https://www.jianshu.com/p/68f9cd012de8
CAS :Compare and Swap,即比较再交换。
CAS算法理解 :CAS是一种无锁算法,CAS有3个操作数,内存值E,旧的预期值V,要修改的新值N。当且仅当预期值V和内存值E相同时,将内存值E修改为N,否则什么都不做。
CAS算法图解 :
上图描述了CAS的原理,以及带来的三大问题以及问题出现的位置。
1.ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查的时候发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前的标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值。
2.循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空,从而提高CPU的实行效率。
3.只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ji=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之前的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
面试被问到CAS原理,触及知识盲区,脸都绿了!
前提知识掌握
想要了解 CAS 底层原理,那么我们先来了解一下 java.uit.concurrent即JUC包的一个类即:AtomicInteger
。
那么 这个 AtomicInteger
有什么用呢 ?
我们平时开发或多或少都会使用到 i++ 这个操作,那么稍微了解多线程的同学都会知道,在多线程环境下,i++ 操作是线程不安全的操作,譬如下面这段代码:
public class Main {
private int i = 0;
public void addI() {
i++;
}
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
for (int i = 1; i <= 100; i++) {
new Thread(() -> {
for (int j = 1; j <= 200; j++) {
main.addI();
}
},String.valueOf(i)).start();
}
//两个线程 主线程 和 垃圾回收线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\\t" + main.i);
}
}
这段代码很简单,就是开启100个线程,每个线程进行200次 i++操作,那么我们来看看最后的答案:
正常来说这个 i 最后的值应该是 20000,但是这里却是19884,当然来,并不是这个值每次都是19884,小伙伴们可以多试几次,但这些值都有一个共同的特性,就是基本上都是小于20000的(当然可能有时候就是20000,可以把线程数开多点,或者每个线程进行 i++次数弄多点)。
这是为什么呢?
原因就是:i++其实不是一个原子性操作,什么叫原子性操作?简单来说就是不可再拆分的操作,而 i++是可以继续拆分的操作,即非原子性操作,它的操作可以分为以下三步:
- 取出 i 的值
- 将 i 的值 进行 +1
- 将 i+1 后的值写回给 i(也就是写回给主内存)
这里涉及到 Java内存模型,不清楚的小伙伴可以参考参考这篇博文哦 ——> Java 内存模型(JMM)、happens-before规则(先行发生规则) 你能知多少?
假设: 现在 i = 1,在多线程环境中,可能线程A正在进行第二步操作,但此时线程B飞快地完成这三部操作,那么此时 i 的值为2,但是线程A并不知道 i 已经为2了,它还会继续往下执行,那么对于线程A来说 会将 i+1的值写回给 i 也就是将 2写回给i,那么最后 i 的值为2不是3就错误了。
那么就会有小伙伴说了,照你这么说直接用 volatile 修饰 i 不就行了。
话不多说,先上代码
这是为什么呢?
我们都知道 volatile进行写操作时会把最新的变量值强制刷新到主内存,还会导致其他线程里的缓存无效。也就是说,当线程B完成i++操作之后,线程A缓存的 i 的值会无效。
但是
我们设想一下这样的场景, i = 10,如果线程A完成i+1操作后准备写回到主内存时阻塞了,此时B线程来了,上来就是一套 接——>化——>发,哦不不不,是一套拷贝、赋值、写入,啪一下,很快啊,B就搞完了,此时 i 为11,那么虽说由volatile保证可见性,但是A线程的+1操作已经完成,准备写入,所以此时A没有阻塞之后,继续进行写入操作,最终 i 的值就是A写入的值,即11,所以没有保证原子性,出现了线程不安全问题。
那么到底要怎么解决呢?
就是使用我一开始提到的 AtomicInteger
解决这类问题,代码如下:
可以看到结果是正确的,为此我还专门 调大了线程数和每个线程的i++次数,可结果仍然正确。
但这些只是前戏
什么是CAS?
CAS中文翻译就是 比较并交换(Compare And Swap)
CAS操作包括了3个操作数:
- 需要读写的内存位置(V)
- 进行比较的预期值(A)
- 拟写入的新值(B)
当且仅当如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
但是既然涉及到了 交换 ,那么怎么保证原子性呢 ?
答案:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。
CAS原理
那么今天的重点就是:CAS的实现原理,而AtomicInteger底层就是使用到的CAS。
public class Main {
public static void main(String[] args) throws InterruptedException {
AtomicInteger num = new AtomicInteger(0);
num.getAndIncrement();
}
}
而 compareAndSwapInt 是个 native方法
可以看见 getAndIncrement() 方法返回的是 unsafe.getAndAddInt(this, valueOffset, 1);
那么,这个 this就是当前的 AtomicInteger 对象,而valueOffset 是当前 AtomicInteger对象 value值的内存偏移量,而1 是每次需要增加的值。
那么继续往下走就是 getAndAddInt() 方法
根据 一 一对应关系
- var1 = this
- var2 = valueOffset
- var4 = 1
而 this.getIntVolatile(var1, var2); 可以理解为获取 var1对象对应的内存偏移量var2对应的值,赋值给 var5
最后是 compareAndSwapInt(var1, var2, var5, var5 + var4),即理解为如果当前 var1对象对应的var4内存偏移量所对应的值与var5相等的话,就对值进行更新。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
完犊子,不知不觉把 AtomicInteger 原理也讲了,这不就少写一篇博文,🤣
既然讲了,那就小总结一下吧:
AtomicInteger实现原理简单来说就是 Unsafe类 加 自旋(do while循环),如果在详细点就说 CAS嘛。
也可以说AtomicInteger是 CAS的一个使用场景,不仅是AtomicInteger,还有各种Atomic开头的原子类,内部都应用到了CAS。
而且上文中涉及到的 Unsafe类是 CAS的核心类,因为Java方法无法访问底层系统,所以需要通过 上文中的 compareAndSwapInt 这样的 native方法来进行访问。Unsafe类存在于sun.misc包中,提供硬件级别的原子操作。
CAS带来的问题
- ABA问题 解决办法 → 如何解决 CAS 的 ABA问题
- 如果自旋长时间不成功,那么循环开销时间大
- 只能保证一个共享变量的操作 → 可以使用 AtomicReference 解决
本篇博文到此页也结束了
等等
读者大大:好歹总结一下CAS原理吧
皮皮虾:时刻为您效劳,只要点赞什么都好说(手动滑稽)
CAS原理总结,面试问到了到底该怎么说?
答:CAS是一种乐观锁机制,具体实现为 Unsafe类 + 自旋,通过Unsafe类提供硬件级别的原子性操作保证了并发安全,加上自旋操作,解决了 synchronized 在多线程环境下会出现的线程阻塞,唤醒切换,以及用户态内核态间的切换操作所带来的消耗。在由此扩展到 AtomicInteger
也就是我上面讲的一套说给面试官,绝对 OK!
尾言
我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!
创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以一键三连哦!,感谢支持,我们下次再见~~~
分享大纲
更多精彩内容分享,请点击 Hello World (●’◡’●)
以上是关于CAS原理以及CAS带来的三大问题的主要内容,如果未能解决你的问题,请参考以下文章