什么是CAS机制?如何解决ABA问题?

您所在的位置:网站首页 java多线程容易出现的问题及解决方案 什么是CAS机制?如何解决ABA问题?

什么是CAS机制?如何解决ABA问题?

#什么是CAS机制?如何解决ABA问题?| 来源: 网络整理| 查看: 265

你知道什么是CAS机制吗?CAS和Synchronized的区别是什么?适用场景呢?优点与缺点呢?

我们先来看一手代码:

启动两个线程,每个线程中让静态变量count循环累加100次。

在这里插入图片描述

该代码输出结果如下。因为这段代码是线程不安全的,所以自增结果很可能会小于200. 在这里插入图片描述

我们加上synchronized同步锁,再来看一下。

在这里插入图片描述

输出结果如下: 在这里插入图片描述

加了同步锁后,count自增的操作变成了原子性操作,所以最终输出结果一定是200,代码实现了线程安全。虽然synchronized确保了线程的安全,但是在有些情况下,这并不是最好的选择。

关键在于性能问题。

synchronized关键字会让没获得锁资源的线程进入BLOCKED(阻塞)状态,只有在争夺到锁资源的时候才转换成RUNNABLE(运行)状态。这其中涉及到操作系统中用户模式和内核模式之间的切换,代价比较高。

同时,尽管jdk对synchronized关键字进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能依然比较低,所以面对这种只对单个变量进行原子性的操作,最好使用jdk自带的“原子操作类”。

原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBoolean、AtomicInteger、AtomicXXX都是分别对应Boolean、Integer或其他类型的原子性操作。

现在我们采用AtomicInteger类试一下:

在这里插入图片描述

输出结果如下: 在这里插入图片描述

使用原子操作类之后,最终的输出结果同样是200,保证了线程安全。并且在某种情况下,该方案代码的性能会比synchronized更好。

而Atomic操作类的底层正是用到了"CAS机制"。

首先,CAS的英文单词是Compare and Swap,即是比较并替换。

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,需要替换的值B。

它的规则是:当需要更新一个变量的值的时候,只有当变量的预期值A和内存地址V中的实际值相同的时候,才会把内存地址V对应的值替换成B。

我们可以来看一个例子:

1.在内存地址V当中,存储着值为10的变量

在这里插入图片描述

2.此时线程1想要把变量的值增加1,对于线程1而言,它旧的预期值A=10,需要替换的最新值B=11。 在这里插入图片描述

3.在线程1要提交更新之前,另外一个线程2抢先一步,将内存地址V中的值更新成了11。

在这里插入图片描述

4.线程1开始提交更新的时候,按照CAS机制,首先进行A的值与内存地址V中的值进行比较,发现A不等于V中的实际值,于是提交失败。

在这里插入图片描述 5.线程1重新获取内存地址V的当前值,并重新计算想要修改的值。在现在而言,线程1旧的预期值A=11,B=12.这个重新尝试的过程被称为自旋。 在这里插入图片描述 6.这一次比较幸运,没有其他线程改变该变量的值,所以线程1进行CAS机制,比较旧的预期值A与内存地址V中的值,发现相同,此时可以替换。 在这里插入图片描述

7.线程1进行替换,把地址V的值替换成B,也就是12.

在这里插入图片描述

从思想上来看,synchronized属于悲观锁,悲观的认为程序中的并发问题十分严重,所以严防死守,只让一个线程操作该代码块。而CAS属于乐观锁,乐观地认为程序中的并发问题并不那么严重,所以让线程不断的去尝试更新。

在java中除了上面提到的Atomic操作类,以及Lock系列类的底层实现,甚至在jdk1.6以上,在synchronized转变为重量级锁之前,也会采用CAS机制。

CAS的优点自然是在并发问题不严重的时候性能比synchronized要快,缺点也有。

CAS的缺点: 1.CPU开销过大

在并发量比较高的时候,如果许多线程都尝试去更新一个变量的值,却又一直比较失败,导致提交失败,产生自旋,循环往复,会对CPU造成很大的压力和开销。

2.不能确保代码块的原子性(注意是代码块)

CAS机制所确保的是一个变量的原子性操作,而不能保证整个代码块的原子性,比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized或者lock了。

3.ABA问题

这就是CAS最大的问题所在。下面说。

讲解了什么是CAS机制、CAS与synchronized的区别、它的优点和缺点之后。 下面我们来介绍两个问题:

1.JAVA中CAS的底层实现。 2.CAS的ABA问题和解决方案。

我们使用idea查看一下AtomicInteger中常用的自增方法incrementAndGet

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

incrementAndGet调用的是unsafe的getAndAddInt方法并增加1

在这里插入图片描述 其中var1是当前对象,var2是内存地址V中的值,var6是旧的期望值A,var6+var4则是需要替换的值B。

可以看到,这一段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事: 1.获取当前的值,该方法使用native实现,底层是用其他语言实现的。

2.当前值+var4,var4就是上一个方法传进来的1,计算出目标值B

3.进行CAS操作,如果成功交换则跳出循环,如果失败则重复以上步骤。

那我们怎么保证valueOffset也就是var2是正确的内存中最新的值的呢?很简单,用volatile关键字来保证线程间的可见性,也就保证了是最新的值。

可以看到我们的代码始终和unsafe这个类相关,那什么是unsafe呢?java语言不像C,C++语言一样可以直接访问底层操作系统,但是JVM为我们开了一个后门,这个后门就是unsafe,unsafe为我们提供了硬件级别的原子操作。

至于valueOffset变量则是通过unsafe的objectFieldOffset获得,所代表的就是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解成value变量的内存地址也就是V了。

我们前面说过,CAS机制中使用了三种基本操作数:内存地址V,旧的期望值A,新的需要替换的值B。

而unsafe的compareAndSwapXxx方法中的参数var2则就代表valueOffset(内存地址V),var6代表A,var6+var4代表B。

正是unsafe的compareAndSwapXxx方法保证了比较和替换之间的原子性操作!

现在我们来说下什么是ABA问题。

1.假设内存中有一个值为A的变量,存储在内存地址V中。 在这里插入图片描述

2.此时有三个线程想要使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。 在这里插入图片描述

3.接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因某种原因阻塞住,没有做更新操作,此时线程3在线程1更新之后,获取了当前值B。 在这里插入图片描述

4.在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成A。 在这里插入图片描述

5.最后,线程2终于恢复了运行状态,由于阻塞之前已经获得到了”当前值A“,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量A的值更新为B。

在这里插入图片描述

看起来这个例子没有什么问题,但如果结合实际,就可以发现它的问题所在。

我们假设一个取款机的例子。假如有一个遵循CAS机制的取款机。小肖有100元存款,需要提取50元。但由于取款机硬件出现了问题,导致取款操作同时提交了两遍,开启了两个线程,两个线程都是获取当前值100元,要更新成50元。

理想情况下,应该一个线程更新成功,一个线程更新失败,小肖的存款只扣除一次,余额为50.

在这里插入图片描述

线程1首先执行成功,把余额100更新为50,同时线程2由于某种原因陷入了阻塞状态,这时候,小肖的妈妈汇款给了小肖50元。

在这里插入图片描述

线程2仍然是阻塞状态,线程3此时执行成功,把余额从50改成了100.

在这里插入图片描述

这时候,线程2恢复运行,由于之前阻塞的时候获得了”当前值“100,并且经过compare检测,此时存款也的确是100元,所以成功把变量值从100更新成50.

在这里插入图片描述

原本线程2应当提交失败,小肖的正确余额应该保持100元,结果由于ABA问题提交成功了。

这就是所谓的ABA问题,那么怎么解决呢?

添加版本号解决ABA问题 真正要做到严谨的CAS机制,我们在compare阶段不仅需要比较内存地址V中的值是否和旧的期望值A相同,还需要比较变量的版本号是否一致。

我们仍然以刚才的例子来说明,假设地址V中存储着变量值A,当前版本号是01.线程1获取了当前值A和版本号01,想要更新为B,此时线程1陷入了阻塞状态。

在这里插入图片描述

这时候,内存地址V中的变量进行了多次改变,版本号提升到03,但是变量值仍然是A。

在这里插入图片描述

随后,线程1恢复运行,进行compare操作。首先经过比较,内存地址V中的值与当前值A相同,但是版本号不相同,所以这一次更新失败。

在这里插入图片描述

在Java中,AtomicStampedReference类就实现了用版本号做比较的CAS机制。

1.Java语言CAS底层是如何实现的? 答:利用unsafe提供的原子性操作方法。 2.什么是ABA问题?怎么解决? 答:当一个值从A更新为B,再从B更新为A,普通CAS机制会误判通过检测。解决方案是使用版本号,通过比较值和版本号才判断是否可以替换。


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3