CAS乐观锁(原子操作)

2023-05-01,,

更多内容,前往 IT-BLOG

锁主要分为两种:乐观锁和悲观锁,而 synchronized 就属于一种悲观锁,每次在操作数据前都会加锁。乐观锁是指:乐观的认为自己在操作数据时,别人不会对当前数据进行修改,因此不会加锁。如果有人对数据进行了修改,则重新获取修改后的数据,进行操作。直到成功为止。而乐观锁的这种机制就是CAS(compare and swap)比较并交换。

一、什么是 CAS


CAS(Compare And Swap | Compare And Set)比较并交换,CAS 是解决多线程并行情况下使用锁造成性能消耗的一种机制。CAS 操作包含三个操作数:内存位置(V)、预期值(A)、新值(B)。如果内存位置的值(V)与逾期原值(A)相同,处理器会将该位置的值更新为新值(B)则 CAS 操作成功。否则,处理器不做任何更改,只需要将当前位置的值进行返回即可。在 Java 可以通过锁和循环 CAS 的方式来实现原子操作。Java 中 java.util.concurrent.atomic 包相关类就是 CAS 的实现。我们就举一个整数的例子:
【1】代码解析:AtomicInteger:通常情况下,在 Java中,i++ 等类似操作并不是线程安全的,因为 i++ 可分为三个独立的操作:获取变量当前值,为该值+1,然后写回新的值。在多线程的情况下 +1000 得到的值往往是不正确的。即使变量被 volatile 修饰,但可以用原子方式 AtomicInteger 自增,这样可以保证数据的原子性。代码如下:

建议:必须具备 volatile 的基本知识,因为 AtomicInteger是 volatile 不具备原子性的解决方案之一。

getAndIncrement 方法:如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。

1 public class CAS {
2 public static void main(String[] args) {
3 //创建一个原子整数,当前值为默认值0
4 AtomicInteger atomicInteger = new AtomicInteger();
5 //调用 CAS 方法进行自增
6 atomicInteger.getAndIncrement();
7 }
8 }

【2】进入 getAndIncrement() 方法,发现底层调用的是 Unsafe 类的 getAndIncrement 方法

Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地方法(native)方法来访问。Unsafe 相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C的指针一样直接操作内存,因为 Java 中 CAS 的操作依赖于 Unsafe 类的方法。注意 Unsafe 类中的所有方法都是 native 修饰的,也就是说 Unsafe 类中的方法都直接调用操作系统底层资源执行相应任务。这种操作时不可分割的,具有原子性。

valueOffset 参数:表示变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的。

1 public final int getAndIncrement() {
2 return unsafe.getAndAddInt(this, valueOffset, 1);
3 }

CAS 是一条 CPU 并发原语(原语属于操作系统范畴,是由若干指令组成,用于完成某个功能的一个过程,并且原语执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS 是一条 CPU 的原子指令,不会造成所谓的数据不一致问题)体现在 Java 语言中就是 sun.misc.Unsafe 类中的各个方法。调用 Unsafe 类中的 CAS方法,JVM 会帮我们编译出 CAS汇编指令。这是一中完全依赖于硬件的功能,通过它实现原子操作。

【3】进入Unsafe 类的 getAndAddInt 方法:我们发现其通过无限循环去解决锁的问题,也称为 “循环锁”,直到修改成功。代码及注释说明如下:

1 public final int getAndAddInt(Object var1, long var2, int var4){
2 int var5;
3 do{
4 //根据对象和地址偏移量获取内存中的值
5 var5 = this.getIntVolatile(var1, var2);
6 //将获取到的值 var5 传入,此方法内部会先比较var2地址的值是否等于 var5,相等则修改var5值并返回,否则重新进入循环。
7 }while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
8 return var5;
9 }

二、CAS 缺点


【1】循环时间长开销很大:自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令,那么效率会有一定的提升,pause 指令有两个作用:第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
【2】只能保证一个共享变量的原子操作:只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference<Clazz> 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。
【3】ABA 问题:因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

三、实战应用


Netty 中的 ByteBuf 的内存回收使用了一种引用计数法的算法,判断当前对象的引用是否为零,如果为零则对对象进行回收。在引用计数的加法的操作,使用到了CAS,代码实例如下:

 1 public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
2 //管理 AbstractReferenceCountedByteBuf 对象中的 refCnt 属性
3 private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater
4 = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
5 private volatile int refCnt = 1;
6 private ByteBuf retain0(int increment) {
7 int refCnt;
8 int nextCnt;
9 do {
10 refCnt = this.refCnt;
11 nextCnt = refCnt + increment;
12 if(nextCnt <= increment) {
13 throw new IllegalReferenceCountException(refCnt, increment);
14 }
15 } while(!refCntUpdater.compareAndSet(this, refCnt, nextCnt));
16
17 return this;
18 }
19 }

 

CAS乐观锁(原子操作)的相关教程结束。

《CAS乐观锁(原子操作).doc》

下载本文的Word格式文档,以方便收藏与打印。