TIP

锁主要分为两种:乐观锁和悲观锁,而 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 方法: 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。

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

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

TIP

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

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

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

TIP

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

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

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

public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
    //管理 AbstractReferenceCountedByteBuf 对象中的 refCnt 属性
    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater
       = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
    private volatile int refCnt = 1;
    private ByteBuf retain0(int increment) {
        int refCnt;
        int nextCnt;
        do {
            refCnt = this.refCnt;
            nextCnt = refCnt + increment;
            if(nextCnt <= increment) {
                throw new IllegalReferenceCountException(refCnt, increment);
            }
        } while(!refCntUpdater.compareAndSet(this, refCnt, nextCnt));

        return this;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(adsbygoogle = window.adsbygoogle || []).push({});