CAS 机制

前言:日常编码过程中,基本不会直接用到 CAS 操作,都是通过一些 JDK 封装好的并发工具类来使用的,在 java.util.concurrent 包下。但是在阅读源码过程中,经常会遇到 CAS。

一、CAS

1、CAS概述

CAS (Compare And Swap,比较与交换) ,底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。

2、无锁实现原理

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意:volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)。

CAS 可以保证更新操作的原子性,通过借助 volatile 读取到共享变量的最新值来实现【比较并交换】的效果,由于内存位置的值与预期原值可能不一样,导致操作失败,为了保证操作的成功,可以使用自旋,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止。

即无锁的原理为:CAS + 自旋 + volatile 变量

3、为什么无锁效率高

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻

线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大

但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

上下文切换成本比较高,需要保存线程的信息,下次线程从阻塞状态恢复到运行状态,需要进行信息的恢复。 

4、CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一 
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

二、Java 中 atomc 包下的原子类

1、原子整数类

JUC 并发包提供了:AtomicBoolean,AtomicInteger,AtomicLong

以 AtomicInteger 为例

AtomicInteger i = new AtomicInteger(0);

// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++ 
System.out.println(i.getAndIncrement());

// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i 
System.out.println(i.incrementAndGet());

// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i 
System.out.println(i.decrementAndGet());

// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i-- 
System.out.println(i.getAndDecrement());

// 获取并加值(i = 0, 结果 i = 5, 返回 0) 
System.out.println(i.getAndAdd(5));

// 加值并获取(i = 5, 结果 i = 0, 返回 0) 
System.out.println(i.addAndGet(-5));

// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0) 
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));

// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0) 
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));

// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0) 
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final 
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));

// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0) 
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

2、原子引用

1)AtomicReference

存在 ABA 问题

public class TestABA {
    
    static AtomicReference ref = new AtomicReference("A");
    
    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        String prev = ref.get();
        other();
        TimeUnit.SECONDS.sleep(2);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
    }

    public static void other(){
        new Thread(() -> {
            log.debug("change A->B {}",ref.compareAndSet(ref.get(),"B"));
        },"t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            log.debug("change B->A {}",ref.compareAndSet(ref.get(),"A"));
        },"t2").start();
    }
}

输出

19:30:15.648 [main] DEBUG c.TestABA - main start...
19:30:15.688 [t1] DEBUG c.TestABA - change A->B true
19:30:16.689 [t2] DEBUG c.TestABA - change B->A true
19:30:18.692 [main] DEBUG c.TestABA - change A->C true

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又  改回 A 的情况,如果主线程希望:

只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。

2)AtomicStampedReference

解决 ABA 问题

public class SloveABA {

    static AtomicStampedReference ref = new AtomicStampedReference("A",1);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start ...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        TimeUnit.SECONDS.sleep(2);
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    public static void other(){
        new Thread(() -> {
            log.debug("A -> B {}",ref.compareAndSet(ref.getReference(),
                    "B",ref.getStamp(),ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        },"t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            log.debug("B -> A {}",ref.compareAndSet(ref.getReference(),
                    "A",ref.getStamp(),ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        },"t2").start();
    }

}

输出

19:33:28.584 [main] DEBUG c.SloveABA - main start ...
19:33:28.586 [main] DEBUG c.SloveABA - 版本 1
19:33:28.619 [t1] DEBUG c.SloveABA - A -> B true
19:33:28.619 [t1] DEBUG c.SloveABA - 更新版本为 2
19:33:29.628 [t2] DEBUG c.SloveABA - B -> A true
19:33:29.628 [t2] DEBUG c.SloveABA - 更新版本为 3
19:33:31.630 [main] DEBUG c.SloveABA - change A->C false

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

3)AtomicMarkableReference

但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 
AtomicMarkableReference

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        AtomicMarkableReference ref = new AtomicMarkableReference(bag, true);

        log.debug("main start ...");
        GarbageBag prev = ref.getReference();
        log.debug(prev.getDesc());

        new Thread(() -> {
            log.debug("打扫卫生的线程 start ...");
            bag.setDesc("空垃圾袋");
            while (!ref.compareAndSet(bag,bag,true,false)){

            };
            log.debug(bag.toString());
        },"t1").start();

        TimeUnit.SECONDS.sleep(1);

        log.debug("主线程想换一只新垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("换了么?" + success);
        log.debug(ref.getReference().toString());
    }
}
class GarbageBag {
    private String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "GarbageBag{" +
                "desc='" + desc + '\'' +
                '}';
    }
}

输出

19:39:32.139 [main] DEBUG c.Test2 - main start ...
19:39:32.141 [main] DEBUG c.Test2 - 装满了垃圾
19:39:32.175 [t1] DEBUG c.Test2 - 打扫卫生的线程 start ...
19:39:32.175 [t1] DEBUG c.Test2 - GarbageBag{desc='空垃圾袋'}
19:39:33.179 [main] DEBUG c.Test2 - 主线程想换一只新垃圾袋?
19:39:33.179 [main] DEBUG c.Test2 - 换了么?false
19:39:33.179 [main] DEBUG c.Test2 - GarbageBag{desc='空垃圾袋'}

3、原子数组

AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

    /*
    参数1 提供数组、可以是线程不安全数组或线程安全数组
    参数2 获取数组长度的方法
    参数3 自增方法,回传 array, index
    参数4 打印数组的方法
     */
    public static  void demo(
            Supplier arraySuppiler, // Supplier    T get();
            Function lengthFun, // Function    R apply(T t)
            BiConsumer putConsumer, // BiConsumer void accept(T t, U u)
            Consumer printConsumer // Consumer    void accept(T t)
    ){
        List ts = new ArrayList<>();
        // 获取数组
        T array = arraySuppiler.get();
        //获取数组长度
        int length = lengthFun.apply(array);

        for (int k = 0; k < 5; k++) {
            ts.add(new Thread(() -> {
                //对数组的每个下标做2000次自增运算
                for (int i = 0;i < 2000;i++) {
                    for (int j = 0; j < length; j++) {
                        putConsumer.accept(array, j);
                    }
                }
            }));
        }

        // 启动所有线程
        ts.forEach(t -> t.start());
        // 等所有线程结束
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        printConsumer.accept(array);
    }
        demo(
                ()-> new AtomicIntegerArray(10),
                (array) -> array.length(),
                (array, index) -> array.getAndIncrement(index),
                array -> System.out.println(array)
        );

输出: 

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

4、字段更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
public class Test1 {

    private volatile int field;

    public static void main(String[] args) {

        AtomicIntegerFieldUpdater field = AtomicIntegerFieldUpdater.newUpdater(Test1.class, "field");

        Test1 test1 = new Test1();

        field.compareAndSet(test1,0,10);
        System.out.println("修改后的field: " + test1.field);

        field.compareAndSet(test1,10,20);
        System.out.println("修改后的field: " + test1.field);

        field.compareAndSet(test1,10,30);
        System.out.println("修改后的field: " + test1.field);
    }
}

输出:

修改后的field: 10
修改后的field: 20
修改后的field: 20

三、AtomicInteger 源码分析 

以AtomicInteger 类的 getAndIncrement()方法源码为例

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long valueOffset; //内存偏移量
    private static final Unsafe unsafe = Unsafe.getUnsafe(); //给Unsafe类的初始化,方便方法中调用。
    
	static {
        try {
            //给valueOffset初始化
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); 
        } catch (Exception ex) { throw new Error(ex); }
    }
 
    // 普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。
    private volatile int value;
    
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
}

valueOffset 是什么? 

valueOffset 所代表的是AtomicInteger对象的value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存当前值。也就是说,valueOffset 是当前AtomicInteger对象初始化时的原始值的内存地址。例如:AtomicInteger atomicInteger = new AtomicInteger(5); 这个5就是原始值,即valueOffset 是5的内存地址。

 CAS + 自旋,如果成功了就跳出循环,如果不成功就再重新尝试,直到成功为止

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 由于 value 声明为 volatile,所以以这种方式读取
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 
        return var5;
    }
 
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    // 在对象指定偏移地址处 volatile 读取一个int
    public native int getIntVolatile(Object var1, long var2);

结论:AtomicInteger 通过 CAS + 自旋 + volatile  value 保证自增方法成功执行。

四、CAS 的缺点

1、循环时间长导致CPU开销大

在并发量比较高的情况下,自旋CAS(不成功就一直循环执行,直到成功) 如果长时间不成功,会给CPU带来非常大的执行开销。

如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

2、只能保证一个共享变量的原子操作

CAS机制保证的只有一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证多个变量共同进行原子性的更新,循环CAS就无法保证操作的原子性,就不得不使用Synchronized了。

或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

3、CAS带来的ABA问题

ABA问题就是,假如Thread1和Thread2先后从内存中拿到值A。这时,Thread1把内存中的值A改为了值B,突然又来了个Thread3,它拿到内存中的值B后,然后又将内存中的值B改为了值A。这过程中,Thread2全然不知。 尽管Thread2的CAS操作成功,但是不代表这个过程就是没有问题的。

ABA问题解决:在JDK中有就提供了java.util.concurrent.atomic.AtomicStampedReference类。AtomicStampReference在cas的基础上增加了一个标记stamp,使用这个标记可以用来觉察数据是否发生变化。

五、CAS的使用场景

CAS 适用于读多写少的情况下,这样冲突一般较少;而synchronized 适用于写多读少的情况下,冲突一般较多。 

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞,发生线程上下文切换,消耗 CPU 资源;而 CAS 操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

参考文章:(2条消息) CAS机制专篇,超详细讲解!(比较与交换,compareAndSwap,CAS原理,Unsafe是什么?valueOffset是什么?CAS的缺点,CAS的应用场景,ABA问题,ABA问题的解决办法)_程序员不相信秃头的博客-CSDN博客_valueoffset

你可能感兴趣的:(juc,juc)